歡迎光臨
每天分享高質量文章

一篇短文帶您瞭解一下EasyCaching

前言

從2017年11月11號在Github創建EasyCaching這個倉庫,到現在也已經將近一年半的時間了,基本都是在下班之後和假期在完善這個專案。

由於EasyCaching目前只有英文的文件托管在Read the Docs上面,當初選的MkDocs現在還不支持多語言,所以這個中文的要等它支持之後才會有計劃。

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

下麵就先簡單介紹一下EasyCaching。

什麼是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比較熟悉,因為兩者的定位和功能都差不多,所以偶爾會聽到有朋友拿這兩個去對比。

為了大家可以更好的進行對比,下麵就重點介紹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)
{
    
    services.AddEasyCaching(option => 
    {
        
        option.UseInMemory("default");
    });    
}    

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    
    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
{
    
    private readonly IEasyCachingProvider _provider;

    public ValuesController(IEasyCachingProvider provider)
    {
        this._provider = provider;
    }
    
    
    [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");
        
        
        return "sync";
    }
    
    
    [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");
        
        
        return "async";
    }
}

還有一個要註意的地方是,如果用的get方法是帶有查詢的,它在沒有命中快取的情況下去資料庫查詢前,會有一個加鎖操作,避免一個key在同一時刻去查了n次資料庫,這個鎖的生存時間和休眠時間是由配置中的LockMsSleepMs決定的。

分佈式快取的序列化選擇

對於分佈式快取的操作,我們不可避免的會遇到序列化的問題.

目前這個主要是針對redis和memcached的。當然,對於序列化,都會有一個預設的實現是基於BinaryFormatter,因為這個不依賴於第三方的類庫,如果沒有指定其它的,就會使用這個去進行序列化的操作了。

除了這個預設的實現,還提供了三種額外的選擇。Newtonsoft.Json,MessagePack和Protobuf。下麵以在Redis的provider使用MessagePack為例,來看看它的用法。

services.AddEasyCaching(option=> 
{
    
    option.UseRedis(config => 
    {
        config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", 6379));
    }, "redis1")
    
    .WithMessagePack();
}); 

不過這裡需要註意的是,目前這些Serializer並不會跟著Provider走,意思就是不能說這個provider用messagepack,那個provider用json,只能有一種Serializer,可能這一個後面需要加強。

多實體支持

可能有人會問多實體是什麼意思,這裡的多實體主要是指,在同一個專案中,同時使用多個provider,包括多個同一型別的provider或著是不同型別的provider。

這樣說可能不太清晰,再來舉一個虛構的小例子,可能大家就會更清晰了。

現在我們的商品快取在redis集群一中,用戶信息在redis集群二中,商品評論快取在mecached集群中,一些簡單的配置信息在應用服務器的本地快取中。

在這種情況下,我們想簡單的通過IEasyCachingProvider來直接操作這麼多不同的快取,顯然是沒辦法做到的!

這個時候想同時操作這麼多不同的快取,就要借助IEasyCachingProviderFactory來指定使用那個provider。

這個工廠是通過provider的名字來獲取要使用的provider。

下麵來看個例子。

我們先添加兩個不同名字的InMemory快取

services.AddEasyCaching(option =>
{
    
    option.UseInMemory("m1");
    
    
    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;  
    }  
  
    
    [HttpGet]  
    [Route("")]  
    public string Get()  
    {  
        
        var provider_1 = _factory.GetCachingProvider("m1");  
        
        var provider_2 = _factory.GetCachingProvider("m2");
        
        
        
    
        return $"multi instances";                 
    }  
}  

上面這個例子中,provider_1和provider_2是不會互相干擾對方的,因為它們是不同的provider!

直觀感覺,有點類似區域(region)的概念,可以這樣去理解,但是嚴格意義上它並不是區域。

快取的AOP操作

說起AOP,可能大家第一印象會是記錄日誌操作,把引數打一下,結果打一下。

其實這個在快取操作中同樣有簡化的作用。

一般情況下,我們可能是這樣操作快取的。

public async Task GetProductAsync(int id)  
{  
    string cacheKey = $"product:{id}";  
      
    var val = await _cache.GetAsync(cacheKey);  
      
    if(val.HasValue)  
        return val.Value;  
      
    var product = await _db.GetProductAsync(id);  
      
    if(product != null)  
        _cache.Set(cacheKey, product, expiration);  
          
    return val;  
}  

如果使用快取的地方很多,那麼我們可能就會覺得煩鎖。

我們同樣可以使用AOP來簡化這一操作。

public interface IProductService 
{
    [EasyCachingAble(Expiration = 10)]
    Task GetProductAsync(int id);
}

public class ProductService : IProductService
{
    public Task GetProductAsync(int id)
    {
        return Task.FromResult(new Product { ... });   
    }
}

可以看到,我們只要在接口的定義上面加上一個Attribute標識一下就可以了。

當然,只加Attribute,不加配置,它也是不會生效的。下麵以EasyCaching.Interceptor.AspectCore為例,添加相應的配置。

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    services.AddScoped();

    services.AddEasyCaching(options =>
    {
        options.UseInMemory("m1");
    });

    return services.ConfigureAspectCoreInterceptor(options =>
    {
        
        
        options.CacheProviderName = "m1";
    });
}

這兩步就可以讓你在呼叫方法的時候優先取快取,沒有快取的時候會去執行方法。

下麵再來說一下三個Attritebute的一些引數。

首先是三個通用配置

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

EasyCachingAble和EasyCachingPut還有一個同名和配置。

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 =>
{
    
    option.UseInMemory("m1");
    option.UseRedis(config =>
    {
        config.DBConfig.Endpoints.Add(new Core.Configurations.ServerEndPoint("127.0.0.1", 6379));
        config.DBConfig.Database = 5;
    }, "myredis");

    
    option.UseHybrid(config =>
    {
        config.EnableLogging = false;
        
        config.TopicName = "test_topic";
        
        config.LocalCacheProviderName = "m1";
        
        config.DistributedCacheProviderName = "myredis";
    });

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

    
    [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上面回覆。

原文地址:https://www.cnblogs.com/catcher1994/p/10806607.html

已同步到看一看
赞(0)

分享創造快樂