一篇短文带您了解一下EasyCaching

前言

从2017年11月11号在Github建立EasyCaching这个仓库,到如今也已经将近一年半的时间了,基本都是在下班以后和假期在完善这个项目。git

因为EasyCaching目前只有英文的文档托管在Read the Docs上面,当初选的MkDocs如今还不支持多语言,因此这个中文的要等它支持以后才会有计划。github

以前在群里有看到过有人说没找到EasyCaching的相关介绍,这也是为何要写这篇博客的缘由。redis

下面就先简单介绍一下EasyCaching。shell

什么是EasyCaching

EasyCaching,这个名字就很大程度上解释了它是作什么的,easy和caching放在一块儿,其最终的目的就是为了让咱们你们在操做缓存的时候更加的方便。数据库

它的发展大概经历了这几个比较重要的时间节点:编程

  1. 18年3月,在茶叔的帮助下进入了NCC
  2. 19年1月,镇汐大大提了不少改进意见
  3. 19年3月,NopCommerce引入EasyCaching (能够看这个 commit记录)
  4. 19年4月,列入awesome-dotnet-core(本身提pr过去的,有点小自恋。。)

在EasyCaching出来以前,大部分人应该会对CacheManager比较熟悉,由于二者的定位和功能都差很少,因此偶尔会听到有朋友拿这两个去对比。json

为了你们能够更好的进行对比,下面就重点介绍EasyCaching现有的功能了。api

EasyCaching的主要功能

EasyCaching主要提供了下面的几个功能缓存

  1. 统一的抽象缓存接口
  2. 多种经常使用的缓存Provider(InMemory,Redis,Memcached,SQLite)
  3. 为分布式缓存的数据序列化提供了多种选择
  4. 二级缓存
  5. 缓存的AOP操做(able, put,evict)
  6. 多实例支持
  7. 支持Diagnostics
  8. 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次数据库,这个锁的生存时间和休眠时间是由配置中的LockMsSleepMs决定的。

分布式缓存的序列化选择

对于分布式缓存的操做,咱们不可避免的会遇到序列化的问题.

目前这个主要是针对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上面回复。

相关文章
相关标签/搜索