一篇短文帶您瞭解一下EasyCaching

前言

從2017年11月11號在Github建立EasyCaching這個倉庫,到如今也已經將近一年半的時間了,基本都是在下班以後和假期在完善這個項目。git

因爲EasyCaching目前只有英文的文檔託管在Read the Docs上面,當初選的MkDocs如今還不支持多語言,因此這個中文的要等它支持以後纔會有計劃。github

以前在羣裏有看到過有人說沒找到EasyCaching的相關介紹,這也是爲何要寫這篇博客的緣由。redis

下面就先簡單介紹一下EasyCaching。shell

什麼是EasyCaching

EasyCaching,這個名字就很大程度上解釋了它是作什麼的,easy和caching放在一塊兒,其最終的目的就是爲了讓咱們你們在操做緩存的時候更加的方便。數據庫

它的發展大概經歷了這幾個比較重要的時間節點:編程

  1. 18年3月,在茶叔的幫助下進入了NCC
  2. 19年1月,鎮汐大大提了不少改進意見
  3. 19年3月,NopCommerce引入EasyCaching (能夠看這個 commit記錄)
  4. 19年4月,列入awesome-dotnet-core(本身提pr過去的,有點小自戀。。)

在EasyCaching出來以前,大部分人應該會對CacheManager比較熟悉,由於二者的定位和功能都差很少,因此偶爾會聽到有朋友拿這兩個去對比。json

爲了你們能夠更好的進行對比,下面就重點介紹EasyCaching現有的功能了。api

EasyCaching的主要功能

EasyCaching主要提供了下面的幾個功能緩存

  1. 統一的抽象緩存接口
  2. 多種經常使用的緩存Provider(InMemory,Redis,Memcached,SQLite)
  3. 爲分佈式緩存的數據序列化提供了多種選擇
  4. 二級緩存
  5. 緩存的AOP操做(able, put,evict)
  6. 多實例支持
  7. 支持Diagnostics
  8. Redis的特殊Provider

固然除了這8個還有一些比較小的就不在這裏列出來講明瞭。服務器

下面就分別來介紹一下上面的這8個功能。

統一的抽象緩存接口

緩存,自己也能夠算做是一個數據源,也是包含了一堆CURD的操做,因此會有一個統一的抽象接口。面向接口編程,雖然EasyCaching提供了一些簡單的實現,不必定能知足您的須要,可是呢,只要你願意,徹底能夠一言不合就實現本身的provider。

對於緩存操做,目前提供了下面幾個,基本都會有同步和異步的操做。

  • TrySet/TrySetAsync
  • Set/SetAsync
  • SetAll/SetAllAsync
  • Get/GetAsync(with data retriever)
  • Get/GetAsync(without data retriever)
  • GetByPrefix/GetByPrefixAsync
  • GetAll/GetAllAsync
  • Remove/RemoveAsync
  • RemoveByPrefix/RemoveByPrefixAsync
  • RemoveAll/RemoveAllAsync
  • Flush/FlushAsync
  • GetCount
  • GetExpiration/GetExpirationAsync
  • Refresh/RefreshAsync(這個後面會被廢棄,直接用set就能夠了)

從名字的定義,應該就能夠知道它們作了什麼,這裏就不繼續展開了。

多種經常使用的緩存Provider

咱們會把這些provider分爲兩大類,一類是本地緩存,一類是分佈式緩存。

目前的實現有下面五個

  • 本地緩存,InMemory,SQLite
  • 分佈式緩存,StackExchange.Redis,csredis,EnyimMemcachedCore

它們的用法都是十分簡單的。下面以InMemory這個Provider爲例來講明。

首先是經過nuget安裝對應的包。

dotnet add package EasyCaching.InMemory

其次是添加配置

public void ConfigureServices(IServiceCollection services)
{
    // 添加EasyCaching
    services.AddEasyCaching(option => 
    {
        // 使用InMemory最簡單的配置
        option.UseInMemory("default");

        //// 使用InMemory自定義的配置
        //option.UseInMemory(options => 
        //{
        //     // DBConfig這個是每種Provider的特有配置
        //     options.DBConfig = new InMemoryCachingOptions
        //     {
        //         // InMemory的過時掃描頻率,默認值是60秒
        //         ExpirationScanFrequency = 60, 
        //         // InMemory的最大緩存數量, 默認值是10000
        //         SizeLimit = 100 
        //     };
        //     // 預防緩存在同一時間所有失效,能夠爲每一個key的過時時間添加一個隨機的秒數,默認值是120秒
        //     options.MaxRdSecond = 120;
        //     // 是否開啓日誌,默認值是false
        //     options.EnableLogging = false;
        //     // 互斥鎖的存活時間, 默認值是5000毫秒
        //     options.LockMs = 5000;
        //     // 沒有獲取到互斥鎖時的休眠時間,默認值是300毫秒
        //     options.SleepMs = 300;
        // }, "m2");         
        
        //// 讀取配置文件
        //option.UseInMemory(Configuration, "m3");
    });    
}    

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    // 若是使用的是Memcached或SQLite,還須要下面這個作一些初始化的操做
    app.UseEasyCaching();
}

配置文件的示例

"easycaching": {
    "inmemory": {
        "MaxRdSecond": 120,
        "EnableLogging": false,
        "LockMs": 5000,
        "SleepMs": 300,
        "DBConfig":{
            "SizeLimit": 10000,
            "ExpirationScanFrequency": 60
        }
    }
}

關於配置,這裏有必要說明一點,那就是MaxRdSecond的值,由於這個把老貓子大哥坑了一次,因此要拎出來特別說一下,這個值的做用是預防在同一時刻出現大批量緩存同時失效,爲每一個key原有的過時時間上面加了一個隨機的秒數,儘量的分散它們的過時時間,若是您的應用場景不須要這個,能夠將其設置爲0。

最後的話就是使用了。

[Route("api/[controller]")]
public class ValuesController : Controller
{
    // 單個provider的時候能夠直接用IEasyCachingProvider
    private readonly IEasyCachingProvider _provider;

    public ValuesController(IEasyCachingProvider provider)
    {
        this._provider = provider;
    }
    
    // GET api/values/sync
    [HttpGet]
    [Route("sync")]
    public string Get()
    {
        var res1 = _provider.Get("demo", () => "456", TimeSpan.FromMinutes(1));
        var res2 = _provider.Get<string>("demo");
        
        _provider.Set("demo", "123", TimeSpan.FromMinutes(1));
        
        _provider.Remove("demo");
        
        // others..
        return "sync";
    }
    
    // GET api/values/async
    [HttpGet]
    [Route("async")]
    public async Task<string> GetAsync(string str)
    {
        var res1 = await _provider.GetAsync("demo", async () => await Task.FromResult("456"), TimeSpan.FromMinutes(1));
        var res2 = await _provider.GetAsync<string>("demo");
    
        await _provider.SetAsync("demo", "123", TimeSpan.FromMinutes(1));
        
        await _provider.RemoveAsync("demo");
        
        // others..
        return "async";
    }
}

還有一個要注意的地方是,若是用的get方法是帶有查詢的,它在沒有命中緩存的狀況下去數據庫查詢前,會有一個加鎖操做,避免一個key在同一時刻去查了n次數據庫,這個鎖的生存時間和休眠時間是由配置中的LockMsSleepMs決定的。

分佈式緩存的序列化選擇

對於分佈式緩存的操做,咱們不可避免的會遇到序列化的問題.

目前這個主要是針對redis和memcached的。固然,對於序列化,都會有一個默認的實現是基於BinaryFormatter,由於這個不依賴於第三方的類庫,若是沒有指定其它的,就會使用這個去進行序列化的操做了。

除了這個默認的實現,還提供了三種額外的選擇。Newtonsoft.Json,MessagePack和Protobuf。下面以在Redis的provider使用MessagePack爲例,來看看它的用法。

services.AddEasyCaching(option=> 
{
    // 使用redis
    option.UseRedis(config => 
    {
        config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379));
    }, "redis1")
    // 使用MessagePack替換BinaryFormatter
    .WithMessagePack()
    //// 使用Newtonsoft.Json替換BinaryFormatter
    //.WithJson()
    //// 使用Protobuf替換BinaryFormatter
    //.WithProtobuf()
    ;
});

不過這裏須要注意的是,目前這些Serializer並不會跟着Provider走,意思就是不能說這個provider用messagepack,那個provider用json,只能有一種Serializer,可能這一個後面須要增強。

多實例支持

可能有人會問多實例是什麼意思,這裏的多實例主要是指,在同一個項目中,同時使用多個provider,包括多個同一類型的provider或着是不一樣類型的provider。

這樣說可能不太清晰,再來舉一個虛構的小例子,可能你們就會更清晰了。

如今咱們的商品緩存在redis集羣一中,用戶信息在redis集羣二中,商品評論緩存在mecached集羣中,一些簡單的配置信息在應用服務器的本地緩存中。

在這種狀況下,咱們想簡單的經過IEasyCachingProvider來直接操做這麼多不一樣的緩存,顯然是沒辦法作到的!

這個時候想同時操做這麼多不一樣的緩存,就要藉助IEasyCachingProviderFactory來指定使用那個provider。

這個工廠是經過provider的名字來獲取要使用的provider。

下面來看個例子。

咱們先添加兩個不一樣名字的InMemory緩存

services.AddEasyCaching(option =>
{
    // 指定當前provider的名字爲m1
    option.UseInMemory("m1");
    
    // 指定當前provider的名字爲m2
    config.UseInMemory(options => 
    {
        options.DBConfig = new InMemoryCachingOptions
        {
            SizeLimit = 100 
        };
    }, "m2");
});

使用的時候

[Route("api/[controller]")]  
public class ValuesController : Controller  
{  
    private readonly IEasyCachingProviderFactory _factory;  
  
    public ValuesController(IEasyCachingProviderFactory factory)  
    {  
        this._factory = factory;  
    }  
  
    // GET api/values
    [HttpGet]  
    [Route("")]  
    public string Get()  
    {  
        // 獲取名字爲m1的provider
        var provider_1 = _factory.GetCachingProvider("m1");  
        // 獲取名字爲m2的provider
        var provider_2 = _factory.GetCachingProvider("m2");
        
        // provider_1.xxx
        // provider_2.xxx
    
        return $"multi instances";                 
    }  
}

上面這個例子中,provider_1和provider_2是不會互相干擾對方的,由於它們是不一樣的provider!

直觀感受,有點相似區域(region)的概念,能夠這樣去理解,可是嚴格意義上它並非區域。

緩存的AOP操做

提及AOP,可能你們第一印象會是記錄日誌操做,把參數打一下,結果打一下。

其實這個在緩存操做中一樣有簡化的做用。

通常狀況下,咱們多是這樣操做緩存的。

public async Task<Product> GetProductAsync(int id)  
{  
    string cacheKey = $"product:{id}";  
      
    var val = await _cache.GetAsync<Product>(cacheKey);  
      
    if(val.HasValue)  
        return val.Value;  
      
    var product = await _db.GetProductAsync(id);  
      
    if(product != null)  
        _cache.Set<Product>(cacheKey, product, expiration);  
          
    return val;  
}

若是使用緩存的地方不少,那麼咱們可能就會以爲煩鎖。

咱們一樣可使用AOP來簡化這一操做。

public interface IProductService 
{
    [EasyCachingAble(Expiration = 10)]
    Task<Product> GetProductAsync(int id);
}

public class ProductService : IProductService
{
    public Task<Product> GetProductAsync(int id)
    {
        return Task.FromResult(new Product { ... });   
    }
}

能夠看到,咱們只要在接口的定義上面加上一個Attribute標識一下就能夠了。

固然,只加Attribute,不加配置,它也是不會生效的。下面以EasyCaching.Interceptor.AspectCore爲例,添加相應的配置。

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IProductService, ProductService>();

    services.AddEasyCaching(options =>
    {
        options.UseInMemory("m1");
    });

    return services.ConfigureAspectCoreInterceptor(options =>
    {
        // 能夠在這裏指定你要用那個provider
        // 或者在Attribute上面指定
        options.CacheProviderName = "m1";
    });
}

這兩步就可讓你在調用方法的時候優先取緩存,沒有緩存的時候會去執行方法。

下面再來講一下三個Attritebute的一些參數。

首先是三個通用配置

配置名 說明
CacheKeyPrefix 指定生成緩存鍵的前綴,正常狀況下是用在修改和刪除的緩存上
CacheProviderName 能夠指定特殊的provider名字
IsHightAvailability 緩存相關操做出現異常時,是否還能繼續執行業務方法

EasyCachingAble和EasyCachingPut還有一個同名和配置。

配置名 說明
Expiration key的過時時間,單位是秒

EasyCachingEvict有兩個特殊的配置。

配置名 說明
IsAll 這個要搭配CacheKeyPrefix來用,就是刪除這個前綴的全部key
IsBefore 在業務方法執行以前刪除緩存仍是執行以後

支持Diagnostics

爲了方便接入第三方的APM,提供了Diagnostics的支持,便於實現追蹤。

下圖是我司接入Jaeger的一個案例。

二級緩存

二級緩存,多級緩存,其實在緩存的小世界中還算是一個比較重要的東西!

一個最爲頭疼的問題就是不一樣級的緩存如何作到近似實時的同步。

在EasyCaching中,二級緩存的實現邏輯大體就是下面的這張圖。

若是某個服務器上面的本地緩存被修改了,就會經過緩存總線去通知其餘服務器把對應的本地緩存移除掉

下面來看一個簡單的使用例子。

首先是添加nuget包。

dotnet add package EasyCaching.InMemory
dotnet add package EasyCaching.Redis
dotnet add package EasyCaching.HybridCache
dotnet add package EasyCaching.Bus.Redis

其次是添加配置。

services.AddEasyCaching(option =>
{
    // 添加兩個基本的provider
    option.UseInMemory("m1");
    option.UseRedis(config =>
    {
        config.DBConfig.Endpoints.Add(new Core.Configurations.ServerEndPoint("127.0.0.1", 6379));
        config.DBConfig.Database = 5;
    }, "myredis");

    //  使用hybird
    option.UseHybrid(config =>
    {
        config.EnableLogging = false;
        // 緩存總線的訂閱主題
        config.TopicName = "test_topic";
        // 本地緩存的名字
        config.LocalCacheProviderName = "m1";
        // 分佈式緩存的名字
        config.DistributedCacheProviderName = "myredis";
    });

    // 使用redis做爲緩存總線
    option.WithRedisBus(config =>
    {
        config.Endpoints.Add(new Core.Configurations.ServerEndPoint("127.0.0.1", 6379));
        config.Database = 6;
    });
});

最後就是使用了。

[Route("api/[controller]")]  
public class ValuesController : Controller  
{  
    private readonly IHybridCachingProvider _provider;  
  
    public ValuesController(IHybridCachingProvider provider)  
    {  
        this._provider = provider;  
    }  
  
    // GET api/values
    [HttpGet]  
    [Route("")]  
    public string Get()  
    {  
        _provider.Set(cacheKey, "val", TimeSpan.FromSeconds(30));
    
        return $"hybrid";                 
    }  
}

若是以爲不清楚,能夠再看看這個完整的例子EasyCachingHybridDemo

Redis的特殊Provider

你們都知道redis支持多種數據結構,還有一些原子遞增遞減的操做等等。爲了支持這些操做,EasyCaching提供了一個獨立的接口,IRedisCachingProvider。

這個接口,目前也只支持了百分之六七十經常使用的一些操做,還有一些可能用的少的就沒加進去。

一樣的,這個接口也是支持多實例的,也能夠經過IEasyCachingProviderFactory來獲取不一樣的provider實例。

在注入的時候,不須要額外的操做,和添加Redis是同樣的。不一樣的是,在使用的時候,再也不是用IEasyCachingProvider,而是要用IRedisCachingProvider

下面是一個簡單的使用例子。

[Route("api/mredis")]
public class MultiRedisController : Controller
{
    private readonly IRedisCachingProvider _redis1;
    private readonly IRedisCachingProvider _redis2;

    public MultiRedisController(IEasyCachingProviderFactory factory)
    {
        this._redis1 = factory.GetRedisProvider("redis1");
        this._redis2 = factory.GetRedisProvider("redis2");
    }

    // GET api/mredis
    [HttpGet]
    public string Get()
    {
        _redis1.StringSet("keyredis1", "val");

        var res1 = _redis1.StringGet("keyredis1");
        var res2 = _redis2.StringGet("keyredis1");

        return $"redis1 cached value: {res1}, redis2 cached value : {res2}";
    }             
}

除了這些基礎功能,還有一些擴展性的功能,在這裏要很是感謝yrinleung,他把EasyCaching和WebApiClient,CAP等項目結合起來了。感興趣的能夠看看這個項目EasyCaching.Extensions

寫在最後

以上就是EasyCaching目前支持的一些功能特性,若是你們在使用的過程當中有遇到問題的話,但願能夠積極的反饋,幫助EasyCaching變得愈來愈好。

若是您對這個項目有興趣,能夠在Github上點個Star,也能夠加入咱們一塊兒進行開發和維護。

前段時間開了一個Issue用來記錄正在使用EasyCaching的相關用戶和案例,若是您正在使用EasyCaching,而且不介意透露您的相關信息,能夠在這個Issue上面回覆。

相關文章
相關標籤/搜索