一篇短文帶您了解一下EasyCaching


前言

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

由於EasyCaching目前只有英文的文檔托管在Read the Docs上面,當初選的MkDocs現在還不支持多語言,所以這個中文的要等它支持之后才會有計划。

之前在群里有看到過有人說沒找到EasyCaching的相關介紹,這也是為什么要寫這篇博客的原因。

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

什么是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比較熟悉,因為兩者的定位和功能都差不多,所以偶爾會聽到有朋友拿這兩個去對比。

為了大家可以更好的進行對比,下面就重點介紹EasyCaching現有的功能了。

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上面回復。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM