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