從2017年11月11號在Github建立EasyCaching這個倉庫,到如今也已經將近一年半的時間了,基本都是在下班以後和假期在完善這個項目。git
因爲EasyCaching目前只有英文的文檔託管在Read the Docs上面,當初選的MkDocs如今還不支持多語言,因此這個中文的要等它支持以後纔會有計劃。github
以前在羣裏有看到過有人說沒找到EasyCaching的相關介紹,這也是爲何要寫這篇博客的緣由。redis
下面就先簡單介紹一下EasyCaching。shell
EasyCaching,這個名字就很大程度上解釋了它是作什麼的,easy和caching放在一塊兒,其最終的目的就是爲了讓咱們你們在操做緩存的時候更加的方便。數據庫
它的發展大概經歷了這幾個比較重要的時間節點:編程
在EasyCaching出來以前,大部分人應該會對CacheManager比較熟悉,由於二者的定位和功能都差很少,因此偶爾會聽到有朋友拿這兩個去對比。json
爲了你們能夠更好的進行對比,下面就重點介紹EasyCaching現有的功能了。api
EasyCaching主要提供了下面的幾個功能緩存
固然除了這8個還有一些比較小的就不在這裏列出來講明瞭。服務器
下面就分別來介紹一下上面的這8個功能。
緩存,自己也能夠算做是一個數據源,也是包含了一堆CURD的操做,因此會有一個統一的抽象接口。面向接口編程,雖然EasyCaching提供了一些簡單的實現,不必定能知足您的須要,可是呢,只要你願意,徹底能夠一言不合就實現本身的provider。
對於緩存操做,目前提供了下面幾個,基本都會有同步和異步的操做。
從名字的定義,應該就能夠知道它們作了什麼,這裏就不繼續展開了。
咱們會把這些provider分爲兩大類,一類是本地緩存,一類是分佈式緩存。
目前的實現有下面五個
它們的用法都是十分簡單的。下面以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,可能你們第一印象會是記錄日誌操做,把參數打一下,結果打一下。
其實這個在緩存操做中一樣有簡化的做用。
通常狀況下,咱們多是這樣操做緩存的。
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 | 在業務方法執行以前刪除緩存仍是執行以後 |
爲了方便接入第三方的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支持多種數據結構,還有一些原子遞增遞減的操做等等。爲了支持這些操做,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上面回覆。