一篇短文帶您瞭解一下EasyCaching
前言
從2017年11月11號在Github建立 EasyCaching 這個倉庫,到現在也已經將近一年半的時間了,基本都是在下班之後和假期在完善這個專案。
由於EasyCaching目前只有英文的 文件 託管在Read the Docs上面,當初選的MkDocs現在還不支援多語言,所以這個中文的要等它支援之後才會有計劃。
之前在群裡有看到過有人說沒找到EasyCaching的相關介紹,這也是為什麼要寫這篇部落格的原因。
下面就先簡單介紹一下EasyCaching。
什麼是EasyCaching
EasyCaching,這個名字就很大程度上解釋了它是做什麼的,easy和caching放在一起,其最終的目的就是為了讓我們大家在操作快取的時候更加的方便。
它的發展大概經歷了這幾個比較重要的時間節點:
- 18年3月,在茶叔的幫助下進入了NCC
- 19年1月,鎮汐大大提了很多改進意見
- 19年3月,NopCommerce引入EasyCaching (可以看這個 commit記錄 )
- 19年4月,列入 awesome-dotnet-core (自己提pr過去的,有點小自戀。。)
在EasyCaching出來之前,大部分人應該會對 CacheManager 比較熟悉,因為兩者的定位和功能都差不多,所以偶爾會聽到有朋友拿這兩個去對比。
為了大家可以更好的進行對比,下面就重點介紹EasyCaching現有的功能了。
EasyCaching的主要功能
EasyCaching主要提供了下面的幾個功能
- 統一的抽象快取介面
- 多種常用的快取Provider(InMemory,Redis,Memcached,SQLite)
- 為分散式快取的資料序列化提供了多種選擇
- 二級快取
- 快取的AOP操作(able, put,evict)
- 多例項支援
- 支援Diagnostics
- 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次資料庫,這個鎖的生存時間和休眠時間是由配置中的 LockMs
和 SleepMs
決定的。
分散式快取的序列化選擇
對於分散式快取的操作,我們不可避免的會遇到序列化的問題.
目前這個主要是針對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上面回覆。