前言
從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)
{
services.AddEasyCaching(option =>
{
option.UseInMemory("default");
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
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
{
private readonly IEasyCachingProvider _provider;
public ValuesController(IEasyCachingProvider provider)
{
this._provider = provider;
}
[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");
return "sync";
}
[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");
return "async";
}
}
還有一個要註意的地方是,如果用的get方法是帶有查詢的,它在沒有命中快取的情況下去資料庫查詢前,會有一個加鎖操作,避免一個key在同一時刻去查了n次資料庫,這個鎖的生存時間和休眠時間是由配置中的LockMs
和SleepMs
決定的。
分散式快取的序列化選擇
對於分散式快取的操作,我們不可避免的會遇到序列化的問題.
目前這個主要是針對redis和memcached的。當然,對於序列化,都會有一個預設的實現是基於BinaryFormatter,因為這個不依賴於第三方的類庫,如果沒有指定其它的,就會使用這個去進行序列化的操作了。
除了這個預設的實現,還提供了三種額外的選擇。Newtonsoft.Json,MessagePack和Protobuf。下麵以在Redis的provider使用MessagePack為例,來看看它的用法。
services.AddEasyCaching(option=>
{
option.UseRedis(config =>
{
config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379));
}, "redis1")
.WithMessagePack();
});
不過這裡需要註意的是,目前這些Serializer並不會跟著Provider走,意思就是不能說這個provider用messagepack,那個provider用json,只能有一種Serializer,可能這一個後面需要加強。
多實體支援
可能有人會問多實體是什麼意思,這裡的多實體主要是指,在同一個專案中,同時使用多個provider,包括多個同一型別的provider或著是不同型別的provider。
這樣說可能不太清晰,再來舉一個虛構的小例子,可能大家就會更清晰了。
現在我們的商品快取在redis叢集一中,使用者資訊在redis叢集二中,商品評論快取在mecached叢集中,一些簡單的配置資訊在應用伺服器的本地快取中。
在這種情況下,我們想簡單的透過IEasyCachingProvider
來直接操作這麼多不同的快取,顯然是沒辦法做到的!
這個時候想同時操作這麼多不同的快取,就要藉助IEasyCachingProviderFactory
來指定使用那個provider。
這個工廠是透過provider的名字來獲取要使用的provider。
下麵來看個例子。
我們先新增兩個不同名字的InMemory快取
services.AddEasyCaching(option =>
{
option.UseInMemory("m1");
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;
}
[HttpGet]
[Route("")]
public string Get()
{
var provider_1 = _factory.GetCachingProvider("m1");
var provider_2 = _factory.GetCachingProvider("m2");
return $"multi instances";
}
}
上面這個例子中,provider_1和provider_2是不會互相干擾對方的,因為它們是不同的provider!
直觀感覺,有點類似區域(region)的概念,可以這樣去理解,但是嚴格意義上它並不是區域。
快取的AOP操作
說起AOP,可能大家第一印象會是記錄日誌操作,把引數打一下,結果打一下。
其實這個在快取操作中同樣有簡化的作用。
一般情況下,我們可能是這樣操作快取的。
public async Task GetProductAsync(int id)
{
string cacheKey = $"product:{id}";
var val = await _cache.GetAsync(cacheKey);
if(val.HasValue)
return val.Value;
var product = await _db.GetProductAsync(id);
if(product != null)
_cache.Set(cacheKey, product, expiration);
return val;
}
如果使用快取的地方很多,那麼我們可能就會覺得煩鎖。
我們同樣可以使用AOP來簡化這一操作。
public interface IProductService
{
[EasyCachingAble(Expiration = 10)]
Task GetProductAsync(int id);
}
public class ProductService : IProductService
{
public Task GetProductAsync(int id)
{
return Task.FromResult(new Product { ... });
}
}
可以看到,我們只要在介面的定義上面加上一個Attribute標識一下就可以了。
當然,只加Attribute,不加配置,它也是不會生效的。下麵以EasyCaching.Interceptor.AspectCore
為例,新增相應的配置。
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddScoped();
services.AddEasyCaching(options =>
{
options.UseInMemory("m1");
});
return services.ConfigureAspectCoreInterceptor(options =>
{
options.CacheProviderName = "m1";
});
}
這兩步就可以讓你在呼叫方法的時候優先取快取,沒有快取的時候會去執行方法。
下麵再來說一下三個Attritebute的一些引數。
首先是三個通用配置
CacheKeyPrefix | 指定生成快取鍵的字首,正常情況下是用在修改和刪除的快取上 |
CacheProviderName | 可以指定特殊的provider名字 |
IsHightAvailability | 快取相關操作出現異常時,是否還能繼續執行業務方法 |
EasyCachingAble和EasyCachingPut還有一個同名和配置。
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 =>
{
option.UseInMemory("m1");
option.UseRedis(config =>
{
config.DBConfig.Endpoints.Add(new Core.Configurations.ServerEndPoint("127.0.0.1", 6379));
config.DBConfig.Database = 5;
}, "myredis");
option.UseHybrid(config =>
{
config.EnableLogging = false;
config.TopicName = "test_topic";
config.LocalCacheProviderName = "m1";
config.DistributedCacheProviderName = "myredis";
});
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;
}
[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");
}
[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上面回覆。
原文地址:https://www.cnblogs.com/catcher1994/p/10806607.html
朋友會在“發現-看一看”看到你“在看”的內容