一篇短文带您了解一下EasyCaching
前言
从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上面回复。
一篇短文带您了解一下EasyCaching的更多相关文章
- 两篇文章带你走入.NET Core 世界:Kestrel+Nginx+Supervisor 部署上云服务器(二)
背景: 上一篇:两篇文章带你走入.NET Core 世界:CentOS+Kestrel+Ngnix 虚拟机先走一遍(一) 已经交待了背景,这篇就省下背景了,这是第二篇文章了,看完就木有下篇了. 直接进 ...
- 两篇文章带你走入.NET Core 世界:CentOS+Kestrel+Ngnix 虚拟机先走一遍(一)
背景: 上一篇:ASP.Net Core on Linux (CentOS7)共享第三方依赖库部署 已经交待了背景,这篇就省下背景了. 折腾的过程分两步: 第一步是:本机跑虚拟机部署试一下: 第二步是 ...
- 三篇文章带你极速入门php(三)之php原生实现登陆注册
看下成果 ps:纯天然h5,绝不添加任何添加剂(css)以及化学成分(js)(<( ̄ ﹌  ̄)我就是喜欢纯天然,不接受任何反驳) 关于本文 用原生的php和html做了一个登陆注册,大概是可以窥 ...
- 【传】玩转Android---UI篇---ImageButton(带图标的按钮)
原文网址:http://hualang.iteye.com/blog/964049 除了Android系统自带的Button按钮一万,还提供了带图标的按钮ImageButton 要制作带图标的按钮,首 ...
- 三篇文章带你极速入门php(一)之语法
本文适合阅读用户 有其他语言基础的童鞋 看完w3cschool语法教程来回顾一下的童鞋(传送门,想全面看一下php语法推荐这里) 毫无基础然而天资聪慧颇有慧根(不要左顾右看说的就是你,老夫这里有一本& ...
- 第十三篇:带缓冲的IO( 标准IO库 )
前言 在之前,学习了 read write 这样的不带缓冲IO函数. 而本文将讲解标准IO库中,带缓冲的IO函数. 为什么要有带缓冲IO函数 标准库提供的带缓冲IO函数是为了减少 read 和 wri ...
- 【赠书】300- 59篇原创带你进入 JavaScript 生态圈
点击上方"前端自习课"关注,学习起来~ 不知不觉,[前端自习课]已经连续推送300天整(不含删除的文章),每天一篇,正如公众号的 Slogan:每日清晨,享受一篇前端优秀文章. 运 ...
- 这篇文章带你彻底理解synchronized
本人免费整理了Java高级资料,涵盖了Java.Redis.MongoDB.MySQL.Zookeeper.Spring Cloud.Dubbo高并发分布式等教程,一共30G,需要自己领取.传送门:h ...
- 一篇blog带你了解java中的锁
前言 最近在复习锁这一块,对java中的锁进行整理,本文介绍各种锁,希望给大家带来帮助. Java的锁 乐观锁 乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人 ...
随机推荐
- VS2017初学者如何打开右侧的解决方案资源管理器
- Sass、LESS 和 Stylus各有千秋
废话不多说直接上连接 为您详细比较三个 CSS 预处理器(框架):Sass.LESS 和 Stylus
- PhpSpreadsheet的简单使用
由于PHPExcel已经不再维护,PhpSpreadsheet是PHPExcel的下一个版本.PhpSpreadsheet是一个用纯PHP编写的库,并引入了命名空间,PSR规范等.这里简单介绍下Php ...
- oracle 查询数据一直提示:“error code [17004]; 无效的列类型”111111
oracle 查询数据一直提示:“error code [17004]; 无效的列类型”111111 问题场景:oracle,jpa,insert原因:插入的字段中有null.导致类型转换出问题,这个 ...
- Web安全测试学习笔记-DVWA-CSRF
CSRF(Cross-site request forgery)跨站请求伪造,CSRF的原理简单来说就是攻击者以用户的名义对服务器发起请求,从而达到攻击目的.与XSS不同之处在于,XSS是盗取用户co ...
- 国产处理器的逆袭机会——RISC-V
前言 今天天气阴沉,刚才又下起了小雨,温度骤降,前几天脱下的秋裤,今天又穿上了,这天气真是变化无常.上周六(4.20)参加了一场关于RSIC-V的技术沙龙,第一次真正了解了RISC-V架构,正好今天不 ...
- .net core 2.1 Swagger 配置
1.先创建 .net core Web 应用程序,选择API 2.安装 Nuget 包:Swashbuckle.AspNetCore Install-Package Swashbuckle.AspNe ...
- ASP.NET Core 2.2 WebApi 系列【四】集成Swagger
Swagger 是一款自动生成在线接口文档+功能测试功能软件 一.安装程序包 通过管理 NuGet 程序包安装,搜索Swashbuckle.AspNetCore 二.配置 Swagger 将 Swag ...
- Web前端基础(9):JavaScript(三)
1. 常用内置对象 所谓内置对象就是ECMAScript提供出来的一些对象,我们知道对象都是有相应的属性和方法. 1.1 数组Array 1.1.1 数组的创建方式 字面量方式创建(推荐大家使用这种方 ...
- Python爬虫,你是否真的了解它?
程序员有时候很难和外行人讲明白自己的工作是什么,甚至有些时候,跟同行的人讲清楚“你是干什么的”也很困难.比如我自己,就对Daivd在搞的语义网一头雾水.所以我打算写一篇博客,讲一下“爬虫工程师”的工作 ...