easycaching:簡單高效的.net緩存包

easycaching:簡單高效的.net緩存包

easycaching,這個名字就很大程度上解釋了它是做什麼的,easy和caching放在一起,其最終的目的就是為了讓我們大家在操作緩存的時候更加的方便。

最后更新 2023/11/5 下午11:14
Catcher Wong
预计阅读 14 分钟
分类
.NET
标签
.NET C#

前言

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

由于 EasyCaching 目前只有英文的文档托管在 Read the Docs 上面,当初选的 MkDocs 现在还不支持多语言,所以这个中文的要等它支持之后才会有计划。

之前在群里有看到過有人說沒找到 easycaching 的相關居間,這也是為什麼要寫這篇博客的原因。

下面就先簡單居間一下 easycaching。

什麼是 easycaching

img

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比较熟悉,因为两者的定位和功能都差不多,所以偶尔会听到有朋友拿这两个去对比。

為了大家可以更好的進行對比,下面就重點居間 easycaching 現有的功能了。

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";
    });
}

這兩步就可以讓你在調用方法的時候優先取緩存,沒有緩存的時候會去執行方法。

下面再來說一下三個 attribute 的一些參數。

首先是三個通用配置

配置名 說明
CacheKeyPrefix 指定生成緩存鍵的前綴,正常情況下是用在修改和刪除的緩存上
CacheProviderName 可以指定特殊的 provider 名字
IsHightAvailability 緩存相關操作出現異常時,是否還能繼續執行業務方法

easycachingable 和 easycachingput 還有一個同名的配置。

配置名 說明
Expiration key 的過期時間,單位是秒

easycachingevict 有兩個特殊的配置。

配置名 說明
IsAll 這個要搭配 cachekeyprefix 來用,就是刪除這個前綴的所有 key
IsBefore 在業務方法執行之前刪除緩存還是執行之後

支持 diagnostics

為了方便接入第三方的 apm,提供了 diagnostics 的支持,便於實現追蹤。

下圖是我司接入 jaeger 的一個案例。

img

二級緩存

二級緩存,多級緩存,其實在緩存的小世界中還算是一個比較重要的東西!

一個最為頭疼的問題就是不同級的緩存如何做到近似實時的同步。

在 easycaching 中,二級緩存的實現邏輯大致就是下面的這張圖。

img

如果某個伺服器上面的本地緩存被修改了,就會通過緩存總線去通知其他伺服器把對應的本地緩存移除掉

下面來看一個簡單的使用例子。

首先是添加 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 上面回复。

img

img

如果您認為這篇文章還不錯或者有所收穫,可以點擊右下角的**【推薦】**按鈕,因為你的支持是我繼續寫作,分享的最大動力!

作者:Catcher Wong ( 黄文清 )

來源:http://catcher1994.cnblogs.com/

聲明: 本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。如果您發現博客中出現了錯誤,或者有更好的建議、想法,請及時與我聯繫!!如果想找我私下交流,可以私信或者加我微信。

Keep Exploring

延伸阅读

更多文章
同分类 / 同标签 2026/2/7

aot使用經驗總結

從項目創建伊始,就應養成良好的習慣,即只要添加了新功能或使用了較新的語法,就及時進行 aot 發布測試。

继续阅读