从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上面回复。