前言
从 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";
});
}
這兩步就可以讓你在調用方法的時候優先取緩存,沒有緩存的時候會去執行方法。
下面再來說一下三個 attribute 的一些參數。
首先是三個通用配置
| 配置名 | 說明 |
|---|---|
| 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 上面回复。


如果您認為這篇文章還不錯或者有所收穫,可以點擊右下角的**【推薦】**按鈕,因為你的支持是我繼續寫作,分享的最大動力!
來源:http://catcher1994.cnblogs.com/
聲明: 本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。如果您發現博客中出現了錯誤,或者有更好的建議、想法,請及時與我聯繫!!如果想找我私下交流,可以私信或者加我微信。