Util應用框架基礎(七) - 緩存

本節介紹Util應用框架如何操作緩存.

概述

緩存是提升性能的關鍵手段之一.

除了提升性能,緩存對系統健壯性和安全性也有影響.

不同類型的系統對緩存的依賴程度不同.

對於後臺管理系統,由於是給管理人員使用的,用戶有限,而且操作基本都需要身份認證和授權,甚至可能部署在局域網內,一般僅對耗時操作使用緩存即可.

但是商城,門戶網站這類系統, 它們部署在互聯網上,並且允許匿名用戶訪問,僅緩存耗時操作是不夠的.

除了訪問量可能比較大,另外需要防範網絡流氓的惡意攻擊,他們會發送大量請求來試探你的系統.

如果某個讀取操作直接到達數據庫,哪怕僅執行非常簡單的SQL,由於請求非常密集,服務器的CPU將很快到達100%從而拒絕服務.

對於這類系統,需要對暴露到互聯網上的所有頁面和讀取數據的API進行緩存.

當然也可以使用更多的只讀數據庫和其它高性能數據庫分攤讀取壓力,本文介紹基於內存和Redis的緩存操作.

緩存框架

要緩存數據,需要選擇一種緩存框架.

.Net 緩存

  • 本地緩存 IMemoryCache

    .Net 提供了 Microsoft.Extensions.Caching.Memory.IMemoryCache 進行本地緩存操作.

    IMemoryCache 可以將數據對象緩存到Web服務器進程的內存中.

    本地緩存的主要優勢是性能非常高,而且不需要序列化對象.

    本地緩存的主要問題是內存容量受限和更新同步困難.

    本地緩存可使用的內存容量受Web服務器內存的限制.

    可以在單體項目中使用本地緩存.

    如果單體項目僅部署一個Web服務器實例,緩存只有一個副本,不存在更新同步的問題.

    但是如果將單體項目部署到Web集羣,由於Web服務器實例不止一個,每個Web服務器都會產生一個緩存副本.

    想要同時更新多個Web服務器的本地緩存非常困難,這可能導致緩存的數據不一致.

    可以使用負載均衡器的會話粘滯特性將用戶每次請求都定位到同一臺Web服務器,從而避免多次請求看到不一致的數據.

    微服務項目情況則更爲複雜,由於包含多個Web Api項目,每個Web Api項目都會部署到一個或多個Web服務器.

    不同 Web Api 項目可能需要使用相同的緩存數據,無法使用負載均衡器的會話粘滯特性解決該問題.

    我們需要使用分佈式緩存來解決內存容量和更新同步的問題.

  • 分佈式緩存 IDistributedCache

    .Net 提供了 Microsoft.Extensions.Caching.Distributed.IDistributedCache 進行分佈式緩存操作.

    IDistributedCache 可以將數據對象序列化後保存到 Redis 等緩存服務器中.

    相比基於內存的本地緩存, 分佈式緩存的性能要低得多, 不僅要序列化對象,還需要跨進程網絡調用.

    但是由於不使用 Web 服務器的內存,所以可以輕鬆的增加緩存容量.

    把緩存抽出來放到專門的服務器後,多個Web Api項目就可以共享緩存.

    由於緩存只有一份,也就不存在同步更新.

  • 直接使用.Net 緩存的問題

    毫無疑問,你可以直接使用 IMemoryCacheIDistributedCache 接口進行緩存操作.

    但會面臨以下問題:

    • Api生硬且不統一.

      IDistributedCache 直接操作 byte[] ,如果不進一步封裝很難使用.

      你需要明確指定是本地緩存還是分佈式緩存,無法使用統一的API,不能通過配置進行切換.

    • 需要自行處理緩存過期引起的性能問題.

      如果同一時間,緩存正好大面積過期,大量請求到達數據庫,從而導致系統可能崩潰,這稱爲緩存雪崩.

      簡單的處理辦法是給每個緩存項設置不同的緩存時間,如果統一配置緩存時間,則添加一個隨機間隔,讓緩存過期的時間錯開即可.

      另一個棘手的問題,如果很多請求併發訪問某個熱點緩存項,當緩存過期,這些併發請求將到達數據庫,這稱爲緩存擊穿.

      雖然只有一個緩存項過期,但還是會損害系統性能.

      可以對併發請求加鎖,只允許第一個進入的請求到達數據庫並更新緩存,後續請求將從更新的緩存讀取.

      爲進一步提升性能,可以在緩存過期前的某個時間更新緩存,從而避免鎖定請求造成的等待.

    • 缺失前綴移除等關鍵特性.

      有些緩存項具有相關性,比如爲當前用戶設置權限,菜單,個人偏好等緩存項,當他退出登錄時,需要清除跟他相關的所有緩存項.

      你可以一個個的移除,但相當費力.

      可以爲具有相關性的緩存項設置相同的緩存前綴,並通過緩存前綴找出所有相關緩存項,從而一次性移除它們.

      遺憾的是, .Net 緩存並不支持這些特性,需要自行實現.

緩存框架 EasyCaching

EasyCaching 是一個專業而易用的緩存框架,提供統一的API接口,並解決了上述問題.

EasyCaching 支持多種緩存提供程序,可以將緩存寫入內存,Redis,Memcached等.

EasyCaching 支持多種序列化方式,可在使用分佈式緩存時指定.

EasyCaching 支持前綴移除,模式移除等高級用法.

除此之外,EasyCaching 還支持2級緩存.

2級緩存可以讓你的項目從本地緩存中獲取數據,這樣可以獲得很高的讀取性能.

當本地緩存過期,本地緩存會請求Redis分佈式緩存,Redis緩存從數據庫讀取最新數據,並更新本地緩存.

Redis還充當事件總線的角色,每當數據更新,通過Redis總線發佈事件,同步更新所有本地緩存副本,解決了本地緩存更新困難的難題.

與 IMemoryCache 相比, EasyCaching 的本地緩存性能稍低,畢竟實現了更多功能.

Util應用框架使用 EasyCaching 緩存框架,並進行簡單包裝.

Util 僅引入了 EasyCaching 的本地緩存Redis緩存兩種提供程序, 以及 SystemTextJson 序列化方式.

如果需要使用其它提供程序和序列化方式,請自行引入相關 Nuget 包.

基礎用法

配置緩存

配置本地緩存

  • 引用Nuget包

    Nuget包名: Util.Caching.EasyCaching

  • AddMemoryCache

    使用 AddMemoryCache 擴展方法啓用本地緩存.

    • 默認配置不帶參數,設置以下默認值:

      • MaxRdSecond 設置爲 1200秒.

      • CacheNulls 設置爲 true.

      var builder = WebApplication.CreateBuilder( args );
      builder.AsBuild().AddMemoryCache();
      
    • 使用 IConfiguration 進行配置.

      可以使用 appsettings.json 文件進行配置.

      var builder = WebApplication.CreateBuilder( args );
      builder.AsBuild().AddMemoryCache( builder.Configuration );
      

      默認配置節: EasyCaching:Memory

      appsettings.json 配置文件示例.

      {
        "EasyCaching": {
          "Memory": {
            "MaxRdSecond": 1200,
            "CacheNulls": true
          }
        }
      }
      
    • 使用委託進行配置.

      var builder = WebApplication.CreateBuilder( args );
      builder.AsBuild().AddMemoryCache( options => {
          options.MaxRdSecond = 1200;
          options.CacheNulls = true;
      } );
      

配置Redis緩存

  • 引用Nuget包

    Nuget包名: Util.Caching.EasyCaching

  • AddRedisCache

    使用 AddRedisCache 擴展方法啓用Redis緩存.

    • 最簡單的配置方法只需傳入Redis服務地址,並設置以下默認值.

      • MaxRdSecond 設置爲 1200秒.

      • CacheNulls 設置爲 true.

      • AllowAdmin 設置爲 true.

      • 端口設置爲 6379.

      • SerializerName 設置爲 "SystemTextJson".

      範例:

      var builder = WebApplication.CreateBuilder( args );
      builder.AsBuild().AddRedisCache( "127.0.0.1" );
      

      如果要修改端口爲 6666,如下所示.

      builder.AsBuild().AddRedisCache( "127.0.0.1",6666 );
      

      還可以統一設置緩存鍵前綴,下面的示例將緩存鍵前綴設置爲 "test:".

      builder.AsBuild().AddRedisCache( "127.0.0.1",6666,"test:" );
      
    • 使用 IConfiguration 進行配置.

      可以使用 appsettings.json 文件進行配置.

      builder.AsBuild().AddRedisCache( builder.Configuration );
      

      默認配置節: EasyCaching:Redis

      appsettings.json 配置文件示例.

      {
        "EasyCaching": {
          "Redis": {
            "MaxRdSecond": 1200,
            "CacheNulls": true,
            "DbConfig": {
              "AllowAdmin": true,
              "Endpoints": [
                {
                  "Host": "localhost",
                  "Port": 6739
                }
              ],
              "Database": 0
            }
          }
        }
      }
      
    • 使用委託進行配置.

      builder.AsBuild().AddRedisCache( options => {
          options.MaxRdSecond = 1200;
          options.CacheNulls = true;        
          options.DBConfig.AllowAdmin = true;
          options.DBConfig.KeyPrefix = "test:";
          options.DBConfig.Endpoints.Add( new ServerEndPoint( "127.0.0.1", 6379 ) );
      } );
      

配置二級緩存

  • 引用Nuget包

    Nuget包名: Util.Caching.EasyCaching

  • AddHybridCache

    使用 AddHybridCache 擴展方法啓用2級緩存.

    • 最簡單的配置方法不帶參數,設置以下默認值.

      • TopicName 設置爲 EasyCachingHybridCache.

        TopicName 是Redis總線發佈事件的主題名稱.

      啓用2級緩存之前,應先配置本地緩存和Redis緩存.

      範例:

      var builder = WebApplication.CreateBuilder( args );
      builder.AsBuild()
        .AddMemoryCache()
        .AddRedisCache( "127.0.0.1" )
        .AddHybridCache();
      

      如果要修改 TopicName,傳入主題參數,如下所示.

      builder.AsBuild()
        .AddMemoryCache()
        .AddRedisCache( "127.0.0.1" )
        .AddHybridCache( "topic" );
      
    • 使用 IConfiguration 進行配置.

      可以使用 appsettings.json 文件進行配置.

      builder.AsBuild()
        .AddMemoryCache()
        .AddRedisCache( "127.0.0.1" )
        .AddHybridCache( builder.Configuration );
      

      除了需要配置2級緩存提供程序,還需要配置 Redis 總線.

      默認配置節名稱:

      • 2級緩存默認配置節名稱: EasyCaching:Hybrid

      • Redis總線配置節名稱: EasyCaching:RedisBus

      appsettings.json 配置文件示例.

      {
        "EasyCaching": {
          "Hybrid": {
            "LocalCacheProviderName": "DefaultInMemory",
            "DistributedCacheProviderName": "DefaultRedis",
            "TopicName": "EasyCachingHybridCache"
          },
          "RedisBus": {
            "Endpoints": [
              {
                "Host": "localhost",
                "Port": 6739
              }
            ],
            "SerializerName": "SystemTextJson"
          }
        }
      }
      
    • 使用委託進行配置.

      builder.AsBuild()
        .AddMemoryCache()
        .AddRedisCache( "127.0.0.1" )
        .AddHybridCache( hybridOptions => {
          hybridOptions.LocalCacheProviderName = "DefaultInMemory";
          hybridOptions.DistributedCacheProviderName = "DefaultRedis";
          hybridOptions.TopicName = "topic";
        }, redisBusOptions => {
            redisBusOptions.Endpoints.Add( new ServerEndPoint( "127.0.0.1", 6379 ) );
            redisBusOptions.SerializerName = "SystemTextJson";
        } )
      
  • 配置參數

    EasyCaching 緩存提供了多個配置參數,具體請參考 EasyCaching 文檔.

    下面介紹幾個比較重要的參數.

    • MaxRdSecond

      MaxRdSecond 是額外添加的緩存間隔最大隨機秒數.

      MaxRdSecond 用於防止緩存雪崩,在緩存時間基礎上增加隨機秒數,以防止同一時間所有緩存項失效.

      MaxRdSecond 的默認值爲 120, 增加的隨機間隔是120秒以內的某個隨機值.

      你可以增大 MaxRdSecond ,以更大的範圍錯開各緩存項的失效時間.

      對於集成測試,你如果要測試緩存失效時間,需要將該值設置爲 0.

    • CacheNulls

      CacheNulls 用於解決緩存穿透問題.

      當使用 Get( key, ()=> value ) 方法獲取緩存時,如果返回的value爲null,是否應該創建緩存項.

      CacheNulls 的值爲 true 時,創建緩存項.

      如果返回值爲null不創建緩存項,使用相同緩存鍵的每次請求都會到達數據庫.

      CacheNulls設置爲 true 可防範正常業務的緩存穿透.

      但惡意攻擊每次傳遞的參數可能不同,請求依然會到達數據庫,且浪費緩存空間.

      可以通過緩存全部有效參數的方式精確判斷輸入參數是否在有效業務範圍,不過會佔用過多內存.

      要減少內存佔用,可使用布隆過濾器.

      EasyCaching尚未內置布隆過濾器,請自行實現.

緩存鍵

每個緩存項有一個唯一標識的鍵名,通過緩存鍵來獲取緩存項.

緩存鍵通常是一個字符串.

可以以任意方式構造緩存鍵,只要保證唯一即可.

但是根據緩存項的功能進行構造更容易識別緩存項的用途.

範例1:

是否管理員緩存鍵

IsAdmin-1

IsAdmin 代表是否管理員, 1是用戶的Id,需要把用戶Id的參數拼接到緩存鍵,以識別特定的緩存項

範例2:

菜單緩存鍵.

Menu-1

Menu 代表菜單, 1是用戶的Id.

緩存鍵前綴

如果用戶退出了,我們需要清除他的全部緩存項.

EasyCaching支持通過緩存鍵前綴批量移除緩存項.

修改前面的範例.

是否管理員緩存鍵: User-1-IsAdmin

菜單緩存鍵: User-1-Menu

User代表用戶,1是用戶Id, User-1 前綴可以標識Id爲1的用戶.

使用 User-1 前綴就可以移除用戶1的所有緩存項.

CacheKey

你可以直接創建緩存鍵字符串,不過有些緩存鍵可能比較複雜,由很多參數構成.

另外可能需要在多個地方使用同一個緩存鍵進行操作.

用一個對象來封裝緩存鍵的構造,不僅可以降低緩存鍵的複雜性,而且也方便多處使用.

Util應用框架提供了一個緩存鍵對象 Util.Caching.CacheKey.

CacheKey 包含兩個屬性, Prefix 和 Key.

Prefix 是緩存鍵前綴,Key是緩存鍵.

通常不直接使用 CacheKey,而是從它派生具體的緩存鍵,這樣可以更清晰的表示緩存項的用途,以及更好的接收參數.

範例:

  • 定義 AclCacheKey 緩存鍵.

    AclCacheKey 表示訪問控制緩存鍵,接收用戶Id和資源Id參數.

    public class AclCacheKey : CacheKey {
        public AclCacheKey( string userId, string resourceId ) {
            Prefix = $"User-{userId}:";
            Key = $"Acl-{resourceId}";
        }
    }
    
  • 使用 AclCacheKey 緩存鍵.

    實例化 AclCacheKey ,傳入參數, 通過 Key 屬性獲取緩存鍵.

    var cacheKey = new AclCacheKey("1","2");
    var key = cacheKey.Key;
    

    Key 屬性返回 Prefix 與 Key 連接後的結果: User-1:Acl-2

    也可以使用 ToString 方法獲取緩存鍵.

    var cacheKey = new AclCacheKey("1","2");
    var key = cacheKey.ToString();
    

緩存操作

Util應用框架緩存操作提供了三個接口: ICache, ILocalCache, IRedisCache.

Util.Caching.ICache 是緩存操作的主要接口.

根據緩存配置,ICache可以在本地緩存,Redis緩存,2級緩存切換.

  • 如果僅配置本地緩存, ICache實例爲本地緩存操作.

  • 如果僅配置 Redis 緩存,ICache實例爲Redis緩存操作.

  • 如果同時配置本地緩存和 Redis 緩存,ICache 實例爲後配置的緩存操作.

  • 如果配置了2級緩存,ICache 實例爲2級緩存操作.

注意事項

如果使用2級緩存,有些操作不可用,調用會拋出異常.

示例上下文

  • 通過依賴注入獲取 ICache 實例.

    public class Service : IService {
      private ICache _cache;
      private IUserResourceRepository _repository;
    
      public Service( ICache cache,IUserResourceRepository repository ) {
          _cache = cache;
          _repository = repository;
      }
    }
    
  • 用戶資源示例

    public class UserResource {
        public string UserId { get; set; }
        public string UserName { get; set; }
        public string ResourceId { get; set; }
        public string ResourceName { get; set; }
    }
    
  • 用戶資源緩存鍵示例

    public class UserResourceCacheKey : CacheKey {
        public UserResourceCacheKey( string userId,string resourceId ) {
            Prefix = $"User-{userId}:";
            Key = $"Resource-{resourceId}";
        }
    }
    
  • 用戶資源倉儲示例

    public interface IUserResourceRepository {
      UserResource GetUserResource( string userId, string resourceId );
      Task<UserResource> GetUserResourceAsync( string userId, string resourceId );
    }
    

API

  • Exists

    功能: 判斷緩存是否存在

    • bool Exists( CacheKey key )

      範例:

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      bool exists = _cache.Exists( cacheKey );
      
    • bool Exists( string key )

      範例:

      bool exists = _cache.Exists( "User-1:Resource-2" );
      
  • ExistsAsync

    功能: 判斷緩存是否存在

    • Task<bool> ExistsAsync( CacheKey key, CancellationToken cancellationToken = default )

      範例:

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      bool exists = await _cache.ExistsAsync( cacheKey );
      
    • Task<bool> ExistsAsync( string key, CancellationToken cancellationToken = default )

      範例:

      bool exists = await _cache.ExistsAsync( "User-1:Resource-2" );
      
  • Get

    功能: 從緩存中獲取數據

    • T Get<T>( CacheKey key )

      範例:

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var result = _cache.Get<UserResource>( cacheKey );
      
    • T Get<T>( string key )

      範例:

      var result = _cache.Get<UserResource>( "User-1:Resource-2" );
      
    • List<T> Get<T>( IEnumerable<CacheKey> keys )

      通過緩存鍵集合獲取結果集合.

      範例:

      var keys = new List<UserResourceCacheKey> { new ( "1", "2" ), new( "3", "4" ) };
      var result = _cache.Get<UserResource>( keys );
      
    • List<T> Get<T>( IEnumerable<string> keys )

      通過緩存鍵集合獲取結果集合.

      範例:

      var keys = new List<string> { "User-1:Resource-2", "User-3:Resource-4" };
      var result = _cache.Get<UserResource>( keys );
      
    • T Get<T>( CacheKey key, Func<T> action, CacheOptions options = null )

      從緩存中獲取數據,如果數據不存在,則執行獲取數據操作並添加到緩存中.

      範例:

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var result = _cache.Get( cacheKey,()=> _repository.GetUserResource( "1", "2" ) );
      

      CacheOptions 配置包含 Expiration 屬性,用於設置緩存過期時間間隔,默認值: 8小時.

      設置1小時過期,如下所示.

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var result = _cache.Get( cacheKey, () => _repository.GetUserResource( "1", "2" ), new CacheOptions { Expiration = TimeSpan.FromHours( 1 ) } );
      
    • T Get<T>( string key, Func<T> action, CacheOptions options = null )

      從緩存中獲取數據,如果數據不存在,則執行獲取數據操作並添加到緩存中.

      範例:

      var result = _cache.Get( "User-1:Resource-2",()=> _repository.GetUserResource( "1", "2" ) );
      
  • GetAsync

    功能: 從緩存中獲取數據

    • Task<object> GetAsync( string key, Type type, CancellationToken cancellationToken = default )

      無法傳入泛型返回類型參數可使用該重載方法.

      範例:

      object result = await _cache.GetAsync( "User-1:Resource-2", typeof( UserResource ) );
      
    • Task<T> GetAsync<T>( CacheKey key, CancellationToken cancellationToken = default )

      範例:

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var result = await _cache.GetAsync<UserResource>( cacheKey );
      
    • Task<T> GetAsync<T>( string key, CancellationToken cancellationToken = default )

      範例:

      var result = await _cache.GetAsync<UserResource>( "User-1:Resource-2" );
      
    • Task<List> GetAsync<T>( IEnumerable<CacheKey> keys, CancellationToken cancellationToken = default )

      通過緩存鍵集合獲取結果集合.

      範例:

      var keys = new List<UserResourceCacheKey> { new ( "1", "2" ), new( "3", "4" ) };
      var result = await _cache.GetAsync<UserResource>( keys );
      
    • Task<List> GetAsync<T>( IEnumerable<string> keys, CancellationToken cancellationToken = default )

      通過緩存鍵集合獲取結果集合.

      範例:

      var keys = new List<string> { "User-1:Resource-2", "User-3:Resource-4" };
      var result = await _cache.GetAsync<UserResource>( keys );
      
    • Task<T> GetAsync<T>( CacheKey key, Func<Task<T>> action, CacheOptions options = null, CancellationToken cancellationToken = default )

      從緩存中獲取數據,如果數據不存在,則執行獲取數據操作並添加到緩存中.

      範例:

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var result = await _cache.GetAsync( cacheKey,async ()=> await _repository.GetUserResourceAsync( "1", "2" ) );
      

      CacheOptions 配置包含 Expiration 屬性,用於設置緩存過期時間間隔,默認值: 8小時.

      設置1小時過期,如下所示.

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var result = await _cache.GetAsync( cacheKey,async ()=> await _repository.GetUserResourceAsync( "1", "2" ), new CacheOptions { Expiration = TimeSpan.FromHours( 1 ) } );
      
    • Task<T> GetAsync<T>( string key, Func<Task<T>> action, CacheOptions options = null, CancellationToken cancellationToken = default )

      從緩存中獲取數據,如果數據不存在,則執行獲取數據操作並添加到緩存中.

      範例:

      var result = await _cache.GetAsync( "User-1:Resource-2",async ()=> await _repository.GetUserResourceAsync( "1", "2" ) );
      
  • GetByPrefix

    功能: 通過緩存鍵前綴獲取數據

    • List<T> GetByPrefix<T>( string prefix )

      範例:

      var result = _cache.GetByPrefix<UserResource>( "User-1" );
      
  • GetByPrefixAsync

    功能: 通過緩存鍵前綴獲取數據

    • Task<List<T>> GetByPrefixAsync<T>( string prefix, CancellationToken cancellationToken = default )

      範例:

      var result = await _cache.GetByPrefixAsync<UserResource>( "User-1" );
      
  • TrySet

    功能: 設置緩存,當緩存已存在則忽略,設置成功返回true

    • bool TrySet<T>( CacheKey key, T value, CacheOptions options = null )

      範例:

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var value = _repository.GetUserResource( "1", "2" );
      var result = _cache.TrySet( cacheKey, value );
      

      CacheOptions 配置包含 Expiration 屬性,用於設置緩存過期時間間隔,默認值: 8小時.

      設置1小時過期,如下所示.

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var value = _repository.GetUserResource( "1", "2" );
      var result = _cache.TrySet( cacheKey, value, new CacheOptions { Expiration = TimeSpan.FromHours( 1 ) } );
      
    • bool TrySet<T>( string key, T value, CacheOptions options = null )

      範例:

      var value = _repository.GetUserResource( "1", "2" );
      var result = _cache.TrySet( "User-1:Resource-2", value );
      
  • TrySetAsync

    功能: 設置緩存,當緩存已存在則忽略,設置成功返回true

    • Task<bool> TrySetAsync<T>( CacheKey key, T value, CacheOptions options = null, CancellationToken cancellationToken = default )

      範例:

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var value = await _repository.GetUserResourceAsync( "1", "2" );
      var result = await _cache.TrySetAsync( cacheKey, value );
      

      CacheOptions 配置包含 Expiration 屬性,用於設置緩存過期時間間隔,默認值: 8小時.

      設置1小時過期,如下所示.

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var value = await _repository.GetUserResourceAsync( "1", "2" );
      var result = await _cache.TrySetAsync( cacheKey, value, new CacheOptions { Expiration = TimeSpan.FromHours( 1 ) } );
      
    • Task<bool> TrySetAsync<T>( string key, T value, CacheOptions options = null, CancellationToken cancellationToken = default )

      範例:

      var value = await _repository.GetUserResourceAsync( "1", "2" );
      var result = await _cache.TrySetAsync( "User-1:Resource-2", value );
      
  • Set

    功能: 設置緩存,當緩存已存在則覆蓋

    • void Set<T>( CacheKey key, T value, CacheOptions options = null )

      範例:

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var value = _repository.GetUserResource( "1", "2" );
      _cache.Set( cacheKey, value );
      

      CacheOptions 配置包含 Expiration 屬性,用於設置緩存過期時間間隔,默認值: 8小時.

      設置1小時過期,如下所示.

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var value = _repository.GetUserResource( "1", "2" );
      _cache.Set( cacheKey, value, new CacheOptions { Expiration = TimeSpan.FromHours( 1 ) } );
      
    • void Set<T>( string key, T value, CacheOptions options = null )

      範例:

      var value = _repository.GetUserResource( "1", "2" );
      _cache.Set( "User-1:Resource-2", value );
      
    • void Set<T>( IDictionary<CacheKey,T> items, CacheOptions options = null )

      範例:

      var items = new Dictionary<CacheKey, UserResource> {
          { new UserResourceCacheKey( "1", "2" ), _repository.GetUserResource( "1", "2" ) },
          { new UserResourceCacheKey( "3", "4" ), _repository.GetUserResource( "3", "4" ) }
      };
      _cache.Set( items );
      
    • void Set<T>( IDictionary<string,T> items, CacheOptions options = null )

      範例:

      var items = new Dictionary<string, UserResource> {
          { "User-1:Resource-2", _repository.GetUserResource( "1", "2" ) },
          { "User-3:Resource-4", _repository.GetUserResource( "3", "4" ) }
      };
      _cache.Set( items );
      
  • SetAsync

    功能: 設置緩存,當緩存已存在則覆蓋

    • Task SetAsync<T>( CacheKey key, T value, CacheOptions options = null, CancellationToken cancellationToken = default )

      範例:

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var value = await _repository.GetUserResourceAsync( "1", "2" );
      await _cache.SetAsync( cacheKey, value );
      

      CacheOptions 配置包含 Expiration 屬性,用於設置緩存過期時間間隔,默認值: 8小時.

      設置1小時過期,如下所示.

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var value = await _repository.GetUserResourceAsync( "1", "2" );
      await _cache.SetAsync( cacheKey, value, new CacheOptions { Expiration = TimeSpan.FromHours( 1 ) } );
      
    • Task SetAsync<T>( string key, T value, CacheOptions options = null, CancellationToken cancellationToken = default )

      範例:

      var value = await _repository.GetUserResourceAsync( "1", "2" );
      await _cache.SetAsync( "User-1:Resource-2", value );
      
    • Task SetAsync<T>( IDictionary<CacheKey, T> items, CacheOptions options = null, CancellationToken cancellationToken = default )

      範例:

      var items = new Dictionary<CacheKey, UserResource> {
          { new UserResourceCacheKey( "1", "2" ), await _repository.GetUserResourceAsync( "1", "2" ) },
          { new UserResourceCacheKey( "3", "4" ), await _repository.GetUserResourceAsync( "3", "4" ) }
      };
      await _cache.SetAsync( items );
      
    • Task SetAsync<T>( IDictionary<string, T> items, CacheOptions options = null, CancellationToken cancellationToken = default )

      範例:

      var items = new Dictionary<string, UserResource> {
          { "User-1:Resource-2", await _repository.GetUserResourceAsync( "1", "2" ) },
          { "User-3:Resource-4", await _repository.GetUserResourceAsync( "3", "4" ) }
      };
      await _cache.SetAsync( items );
      
  • Remove

    功能: 移除緩存

    • void Remove( CacheKey key )

      範例:

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      _cache.Remove( cacheKey );
      
    • void Remove( string key )

      範例:

      _cache.Remove( "User-1:Resource-2" );
      
    • void Remove( IEnumerable<CacheKey> keys )

      範例:

      var keys = new List<UserResourceCacheKey> { new ( "1", "2" ), new( "3", "4" ) };
      _cache.Remove( keys );
      
    • void Remove( IEnumerable<string> keys )

      範例:

      var keys = new List<string> { "User-1:Resource-2", "User-3:Resource-4" };
      _cache.Remove( keys );
      
  • RemoveAsync

    功能: 移除緩存

    • Task RemoveAsync( CacheKey key, CancellationToken cancellationToken = default )

      範例:

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      await _cache.RemoveAsync( cacheKey );
      
    • Task RemoveAsync( string key, CancellationToken cancellationToken = default )

      範例:

      await _cache.RemoveAsync( "User-1:Resource-2" );
      
    • Task RemoveAsync( IEnumerable<CacheKey> keys, CancellationToken cancellationToken = default )

      範例:

      var keys = new List<UserResourceCacheKey> { new( "1", "2" ), new( "3", "4" ) };
      await _cache.RemoveAsync( keys );
      
    • Task RemoveAsync( IEnumerable<string> keys, CancellationToken cancellationToken = default )

      範例:

      var keys = new List<string> { "User-1:Resource-2", "User-3:Resource-4" };
      await _cache.RemoveAsync( keys );
      
  • RemoveByPrefix

    功能: 通過緩存鍵前綴移除緩存

    • void RemoveByPrefix( string prefix )

      範例:

      _cache.RemoveByPrefix( "User-1" );
      
    • Task RemoveByPrefixAsync( string prefix, CancellationToken cancellationToken = default )

      範例:

      await _cache.RemoveByPrefixAsync( "User-1" );
      
  • RemoveByPattern

    功能: 通過模式移除緩存

    • void RemoveByPattern( string pattern )

      範例:

      移除 User 開頭的緩存.

      _cache.RemoveByPattern( "User*" );
      
  • RemoveByPatternAsync

    功能: 通過模式移除緩存

    • Task RemoveByPatternAsync( string pattern, CancellationToken cancellationToken = default )

      範例:

      移除 User 開頭的緩存.

      await _cache.RemoveByPatternAsync( "User*" );
      
  • Clear

    功能: 清空緩存

    • void Clear()

      範例:

      _cache.Clear();
      
  • ClearAsync

    功能: 清空緩存

    • Task ClearAsync( CancellationToken cancellationToken = default )

      範例:

      await _cache.ClearAsync();
      

ILocalCache

Util.Caching.ILocalCache 從 ICache 派生,表示本地緩存.

當同時配置本地緩存和Redis緩存, 如果你想明確使用本地緩存, 請使用 ILocalCache.

Api 參考 ICache.

IRedisCache

Util.Caching.IRedisCache 從 ICache 派生,表示 Redis 分佈式緩存.

當同時配置本地緩存和Redis緩存, 如果你想明確使用 Redis 緩存, 請使用 IRedisCache.

IRedisCache 除了繼承基礎緩存操作外,還將添加 Redis 專用緩存操作.

目前 IRedisCache 尚未添加 Redis 專用操作,後續根據需要進行添加.

Api 參考 ICache.

更新緩存

  • 設置緩存到期時間

    創建緩存項時可以設置一個過期時間間隔,超過到期時間,緩存將失效.

    EasyCaching 目前尚不支持滑動過期.

    下面的示例設置1小時的過期時間間隔,當超過1小時,緩存過期後,將重新加載最新數據.

    var cacheKey = new UserResourceCacheKey( "1", "2" );
    var result = _cache.Get( cacheKey, () => _repository.GetUserResource( "1", "2" ), new CacheOptions { Expiration = TimeSpan.FromHours( 1 ) } );
    
  • 通過本地事件總線更新緩存

    基於過期時間被動更新,適合實時性要求低的場景.

    當數據庫的值已更新,從緩存中讀取舊值,對業務基本沒有影響或影響很小.

    但有些數據具有更高的實時性,在數據庫更新時,需要同步更新緩存中的副本.

    可以通過發佈訂閱本地事件總線實時更新特定緩存.

緩存攔截器

  • CacheAttribute 緩存攔截器

    [Cache] 是一個緩存攔截器,使用 ICache 接口操作緩存.

    它會根據參數自動創建緩存鍵,並調用攔截的方法獲取數據並緩存起來.

    如果你不關心緩存鍵的長相,可以使用 [Cache] 攔截器快速添加緩存.

    範例:

    public interface ITestService {
        [Cache]
        UserResource Get( string userId, string resourceId );
    }
    
    • 設置緩存鍵前綴 Prefix.

      緩存鍵前綴支持佔位符, {0} 代表第一個參數.

      範例:

      public interface ITestService {
          [Cache( Prefix = "User-{0}" )]
          UserResource Get( string userId, string resourceId );
      }
      

      下面的示例調用 ITestService 的 Get 方法,傳入參數 userId = "1" , resourceId = "2" .

      創建的緩存鍵爲: "User-1:1:2".

      緩存鍵前綴 User-{0} 中的 {0} 替換爲第一個參數 userId ,即 User-1.

      使用 : 按順序連接所有參數值.

      var result = _service.Get( "1", "2" );
      
    • 設置緩存過期間隔 Expiration ,單位: 秒,默認值: 36000

      範例:

      設置 120 秒過期.

      public interface ITestService {
          [Cache( Expiration = 120 )]
          UserResource Get( string userId, string resourceId );
      }
      
  • LocalCacheAttribute 本地緩存攔截器

    [LocalCache] 與 [Cache] 類似,但它使用 ILocalCache 接口操作緩存.

    如果你的某個操作需要使用本地緩存,可以用 [LocalCache].

    具體操作請參考 [Cache].

  • RedisCacheAttribute Redis緩存攔截器

    [RedisCache] 與 [Cache] 類似,但它使用 IRedisCache 接口操作緩存.

    如果你的某個操作需要使用Redis緩存,可以用 [RedisCache].

    具體操作請參考 [Cache].

緩存內存釋放

當緩存佔據大量內存空間,調用 Clear 清理緩存並不會釋放內存,等待一段時間仍然不會釋放.

對於 IMemoryCache 同樣如此.

某些測試環境,你可以調用 GC.Collect() 強制回收內存空間.

生產環境,不應手工回收.

源碼解析

ICache 緩存操作

Util.Caching.ICache 是緩存操作接口.

CacheManager 將緩存操作委託給 EasyCaching 的 IEasyCachingProvider 接口.

IEasyCachingProvider 根據配置的提供程序切換爲本地緩存或Redis緩存.

當配置了2級緩存, 緩存操作委託給 IHybridCachingProvider 2級緩存提供程序接口.

/// <summary>
/// 緩存
/// </summary>
public interface ICache {
    /// <summary>
    /// 緩存是否已存在
    /// </summary>
    /// <param name="key">緩存鍵</param>
    bool Exists( CacheKey key );
    /// <summary>
    /// 緩存是否已存在
    /// </summary>
    /// <param name="key">緩存鍵</param>
    bool Exists( string key );
    /// <summary>
    /// 緩存是否已存在
    /// </summary>
    /// <param name="key">緩存鍵</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task<bool> ExistsAsync( CacheKey key, CancellationToken cancellationToken = default );
    /// <summary>
    /// 緩存是否已存在
    /// </summary>
    /// <param name="key">緩存鍵</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task<bool> ExistsAsync( string key, CancellationToken cancellationToken = default );
    /// <summary>
    /// 從緩存中獲取數據
    /// </summary>
    /// <typeparam name="T">緩存數據類型</typeparam>
    /// <param name="key">緩存鍵</param>
    T Get<T>( CacheKey key );
    /// <summary>
    /// 從緩存中獲取數據
    /// </summary>
    /// <typeparam name="T">緩存數據類型</typeparam>
    /// <param name="key">緩存鍵</param>
    T Get<T>( string key );
    /// <summary>
    /// 從緩存中獲取數據
    /// </summary>
    /// <typeparam name="T">緩存數據類型</typeparam>
    /// <param name="keys">緩存鍵集合</param>
    List<T> Get<T>( IEnumerable<CacheKey> keys );
    /// <summary>
    /// 從緩存中獲取數據
    /// </summary>
    /// <typeparam name="T">緩存數據類型</typeparam>
    /// <param name="keys">緩存鍵集合</param>
    List<T> Get<T>( IEnumerable<string> keys );
    /// <summary>
    /// 從緩存中獲取數據,如果不存在,則執行獲取數據操作並添加到緩存中
    /// </summary>
    /// <typeparam name="T">緩存數據類型</typeparam>
    /// <param name="key">緩存鍵</param>
    /// <param name="action">獲取數據操作</param>
    /// <param name="options">緩存配置</param>
    T Get<T>( CacheKey key, Func<T> action, CacheOptions options = null );
    /// <summary>
    /// 從緩存中獲取數據,如果不存在,則執行獲取數據操作並添加到緩存中
    /// </summary>
    /// <typeparam name="T">緩存數據類型</typeparam>
    /// <param name="key">緩存鍵</param>
    /// <param name="action">獲取數據操作</param>
    /// <param name="options">緩存配置</param>
    T Get<T>( string key, Func<T> action, CacheOptions options = null );
    /// <summary>
    /// 從緩存中獲取數據
    /// </summary>
    /// <param name="key">緩存鍵</param>
    /// <param name="type">緩存數據類型</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task<object> GetAsync( string key, Type type, CancellationToken cancellationToken = default );
    /// <summary>
    /// 從緩存中獲取數據
    /// </summary>
    /// <typeparam name="T">緩存數據類型</typeparam>
    /// <param name="key">緩存鍵</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task<T> GetAsync<T>( CacheKey key, CancellationToken cancellationToken = default );
    /// <summary>
    /// 從緩存中獲取數據
    /// </summary>
    /// <typeparam name="T">緩存數據類型</typeparam>
    /// <param name="key">緩存鍵</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task<T> GetAsync<T>( string key, CancellationToken cancellationToken = default );
    /// <summary>
    /// 從緩存中獲取數據
    /// </summary>
    /// <typeparam name="T">緩存數據類型</typeparam>
    /// <param name="keys">緩存鍵集合</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task<List<T>> GetAsync<T>( IEnumerable<CacheKey> keys, CancellationToken cancellationToken = default );
    /// <summary>
    /// 從緩存中獲取數據
    /// </summary>
    /// <typeparam name="T">緩存數據類型</typeparam>
    /// <param name="keys">緩存鍵集合</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task<List<T>> GetAsync<T>( IEnumerable<string> keys, CancellationToken cancellationToken = default );
    /// <summary>
    /// 從緩存中獲取數據,如果不存在,則執行獲取數據操作並添加到緩存中
    /// </summary>
    /// <typeparam name="T">緩存數據類型</typeparam>
    /// <param name="key">緩存鍵</param>
    /// <param name="action">獲取數據操作</param>
    /// <param name="options">緩存配置</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task<T> GetAsync<T>( CacheKey key, Func<Task<T>> action, CacheOptions options = null, CancellationToken cancellationToken = default );
    /// <summary>
    /// 從緩存中獲取數據,如果不存在,則執行獲取數據操作並添加到緩存中
    /// </summary>
    /// <typeparam name="T">緩存數據類型</typeparam>
    /// <param name="key">緩存鍵</param>
    /// <param name="action">獲取數據操作</param>
    /// <param name="options">緩存配置</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task<T> GetAsync<T>( string key, Func<Task<T>> action, CacheOptions options = null, CancellationToken cancellationToken = default );
    /// <summary>
    /// 通過緩存鍵前綴獲取數據
    /// </summary>
    /// <typeparam name="T">緩存數據類型</typeparam>
    /// <param name="prefix">緩存鍵前綴</param>
    List<T> GetByPrefix<T>( string prefix );
    /// <summary>
    /// 通過緩存鍵前綴獲取數據
    /// </summary>
    /// <typeparam name="T">緩存數據類型</typeparam>
    /// <param name="prefix">緩存鍵前綴</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task<List<T>> GetByPrefixAsync<T>( string prefix, CancellationToken cancellationToken = default );
    /// <summary>
    /// 設置緩存,當緩存已存在則忽略,設置成功返回true
    /// </summary>
    /// <typeparam name="T">緩存數據類型</typeparam>
    /// <param name="key">緩存鍵</param>
    /// <param name="value">值</param>
    /// <param name="options">緩存配置</param>
    bool TrySet<T>( CacheKey key, T value, CacheOptions options = null );
    /// <summary>
    /// 設置緩存,當緩存已存在則忽略,設置成功返回true
    /// </summary>
    /// <typeparam name="T">緩存數據類型</typeparam>
    /// <param name="key">緩存鍵</param>
    /// <param name="value">值</param>
    /// <param name="options">緩存配置</param>
    bool TrySet<T>( string key, T value, CacheOptions options = null );
    /// <summary>
    /// 設置緩存,當緩存已存在則忽略,設置成功返回true
    /// </summary>
    /// <typeparam name="T">緩存數據類型</typeparam>
    /// <param name="key">緩存鍵</param>
    /// <param name="value">值</param>
    /// <param name="options">緩存配置</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task<bool> TrySetAsync<T>( CacheKey key, T value, CacheOptions options = null, CancellationToken cancellationToken = default );
    /// <summary>
    /// 設置緩存,當緩存已存在則忽略,設置成功返回true
    /// </summary>
    /// <typeparam name="T">緩存數據類型</typeparam>
    /// <param name="key">緩存鍵</param>
    /// <param name="value">值</param>
    /// <param name="options">緩存配置</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task<bool> TrySetAsync<T>( string key, T value, CacheOptions options = null, CancellationToken cancellationToken = default );
    /// <summary>
    /// 設置緩存,當緩存已存在則覆蓋
    /// </summary>
    /// <typeparam name="T">緩存數據類型</typeparam>
    /// <param name="key">緩存鍵</param>
    /// <param name="value">值</param>
    /// <param name="options">緩存配置</param>
    void Set<T>( CacheKey key, T value, CacheOptions options = null );
    /// <summary>
    /// 設置緩存,當緩存已存在則覆蓋
    /// </summary>
    /// <typeparam name="T">緩存數據類型</typeparam>
    /// <param name="key">緩存鍵</param>
    /// <param name="value">值</param>
    /// <param name="options">緩存配置</param>
    void Set<T>( string key, T value, CacheOptions options = null );
    /// <summary>
    /// 設置緩存集合
    /// </summary>
    /// <typeparam name="T">緩存數據類型</typeparam>
    /// <param name="items">緩存項集合</param>
    /// <param name="options">緩存配置</param>
    void Set<T>( IDictionary<CacheKey,T> items, CacheOptions options = null );
    /// <summary>
    /// 設置緩存集合
    /// </summary>
    /// <typeparam name="T">緩存數據類型</typeparam>
    /// <param name="items">緩存項集合</param>
    /// <param name="options">緩存配置</param>
    void Set<T>( IDictionary<string, T> items, CacheOptions options = null );
    /// <summary>
    /// 設置緩存,當緩存已存在則覆蓋
    /// </summary>
    /// <typeparam name="T">緩存數據類型</typeparam>
    /// <param name="key">緩存鍵</param>
    /// <param name="value">值</param>
    /// <param name="options">緩存配置</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task SetAsync<T>( CacheKey key, T value, CacheOptions options = null, CancellationToken cancellationToken = default );
    /// <summary>
    /// 設置緩存,當緩存已存在則覆蓋
    /// </summary>
    /// <typeparam name="T">緩存數據類型</typeparam>
    /// <param name="key">緩存鍵</param>
    /// <param name="value">值</param>
    /// <param name="options">緩存配置</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task SetAsync<T>( string key, T value, CacheOptions options = null, CancellationToken cancellationToken = default );
    /// <summary>
    /// 設置緩存集合
    /// </summary>
    /// <typeparam name="T">緩存數據類型</typeparam>
    /// <param name="items">緩存項集合</param>
    /// <param name="options">緩存配置</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task SetAsync<T>( IDictionary<CacheKey, T> items, CacheOptions options = null, CancellationToken cancellationToken = default );
    /// <summary>
    /// 設置緩存集合
    /// </summary>
    /// <typeparam name="T">緩存數據類型</typeparam>
    /// <param name="items">緩存項集合</param>
    /// <param name="options">緩存配置</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task SetAsync<T>( IDictionary<string, T> items, CacheOptions options = null, CancellationToken cancellationToken = default );
    /// <summary>
    /// 移除緩存
    /// </summary>
    /// <param name="key">緩存鍵</param>
    void Remove( CacheKey key );
    /// <summary>
    /// 移除緩存
    /// </summary>
    /// <param name="key">緩存鍵</param>
    void Remove( string key );
    /// <summary>
    /// 移除緩存集合
    /// </summary>
    /// <param name="keys">緩存鍵集合</param>
    void Remove( IEnumerable<CacheKey> keys );
    /// <summary>
    /// 移除緩存集合
    /// </summary>
    /// <param name="keys">緩存鍵集合</param>
    void Remove( IEnumerable<string> keys );
    /// <summary>
    /// 移除緩存
    /// </summary>
    /// <param name="key">緩存鍵</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task RemoveAsync( CacheKey key, CancellationToken cancellationToken = default );
    /// <summary>
    /// 移除緩存
    /// </summary>
    /// <param name="key">緩存鍵</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task RemoveAsync( string key, CancellationToken cancellationToken = default );
    /// <summary>
    /// 移除緩存集合
    /// </summary>
    /// <param name="keys">緩存鍵集合</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task RemoveAsync( IEnumerable<CacheKey> keys, CancellationToken cancellationToken = default );
    /// <summary>
    /// 移除緩存集合
    /// </summary>
    /// <param name="keys">緩存鍵集合</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task RemoveAsync( IEnumerable<string> keys, CancellationToken cancellationToken = default );
    /// <summary>
    /// 通過緩存鍵前綴移除緩存
    /// </summary>
    /// <param name="prefix">緩存鍵前綴</param>
    void RemoveByPrefix( string prefix );
    /// <summary>
    /// 通過緩存鍵前綴移除緩存
    /// </summary>
    /// <param name="prefix">緩存鍵前綴</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task RemoveByPrefixAsync( string prefix, CancellationToken cancellationToken = default );
    /// <summary>
    /// 通過緩存鍵模式移除緩存
    /// </summary>
    /// <param name="pattern">緩存鍵模式,範例: test*</param>
    void RemoveByPattern( string pattern );
    /// <summary>
    /// 通過緩存鍵模式移除緩存
    /// </summary>
    /// <param name="pattern">緩存鍵模式,範例: test*</param>
    /// <param name="cancellationToken">取消令牌</param>
    Task RemoveByPatternAsync( string pattern, CancellationToken cancellationToken = default );
    /// <summary>
    /// 清空緩存
    /// </summary>
    void Clear();
    /// <summary>
    /// 清空緩存
    /// </summary>
    /// <param name="cancellationToken">取消令牌</param>
    Task ClearAsync( CancellationToken cancellationToken = default );
}

/// <summary>
/// EasyCaching緩存服務
/// </summary>
public class CacheManager : ICache {

    #region 字段

    /// <summary>
    /// 緩存提供器
    /// </summary>
    private readonly IEasyCachingProviderBase _provider;
    /// <summary>
    /// 緩存提供器
    /// </summary>
    private readonly IEasyCachingProvider _cachingProvider;

    #endregion

    #region 構造方法

    /// <summary>
    /// 初始化EasyCaching緩存服務
    /// </summary>
    /// <param name="provider">EasyCaching緩存提供器</param>
    /// <param name="hybridProvider">EasyCaching 2級緩存提供器</param>
    public CacheManager( IEasyCachingProvider provider, IHybridCachingProvider hybridProvider = null ) {
        CachingOptions.Clear();
        if ( provider != null ) {
            _provider = provider;
            _cachingProvider = provider;
        }
        if( hybridProvider != null )
            _provider = hybridProvider;
        _provider.CheckNull( nameof( provider ) );
    }

    #endregion

    #region Exists

    /// <inheritdoc />
    public bool Exists( CacheKey key ) {
        key.Validate();
        return Exists( key.Key );
    }

    /// <inheritdoc />
    public bool Exists( string key ) {
        return _provider.Exists( key );
    }

    #endregion

    #region ExistsAsync

    /// <inheritdoc />
    public async Task<bool> ExistsAsync( CacheKey key, CancellationToken cancellationToken = default ) {
        key.Validate();
        return await ExistsAsync( key.Key, cancellationToken );
    }

    /// <inheritdoc />
    public async Task<bool> ExistsAsync( string key, CancellationToken cancellationToken = default ) {
        return await _provider.ExistsAsync( key, cancellationToken );
    }

    #endregion

    #region Get

    /// <inheritdoc />
    public T Get<T>( CacheKey key ) {
        key.Validate();
        return Get<T>( key.Key );
    }

    /// <inheritdoc />
    public T Get<T>( string key ) {
        var result = _provider.Get<T>( key );
        return result.Value;
    }

    /// <inheritdoc />
    public List<T> Get<T>( IEnumerable<CacheKey> keys ) {
        return Get<T>( ToKeys( keys ) );
    }

    /// <summary>
    /// 轉換爲緩存鍵字符串集合
    /// </summary>
    private IEnumerable<string> ToKeys( IEnumerable<CacheKey> keys ) {
        keys.CheckNull( nameof( keys ) );
        var cacheKeys = keys.ToList();
        cacheKeys.ForEach( t => t.Validate() );
        return cacheKeys.Select( t => t.Key );
    }

    /// <inheritdoc />
    public List<T> Get<T>( IEnumerable<string> keys ) {
        Validate();
        var result = _cachingProvider.GetAll<T>( keys );
        return result.Values.Select( t => t.Value ).ToList();
    }

    /// <summary>
    /// 驗證
    /// </summary>
    private void Validate() {
        if ( _cachingProvider == null )
            throw new NotSupportedException( "2級緩存不支持該操作" );
    }

    /// <inheritdoc />
    public T Get<T>( CacheKey key, Func<T> action, CacheOptions options = null ) {
        key.Validate();
        return Get( key.Key, action, options );
    }

    /// <inheritdoc />
    public T Get<T>( string key, Func<T> action, CacheOptions options = null ) {
        var result = _provider.Get( key, action, GetExpiration( options ) );
        return result.Value;
    }

    /// <summary>
    /// 獲取過期時間間隔
    /// </summary>
    private TimeSpan GetExpiration( CacheOptions options ) {
        var result = options?.Expiration;
        result ??= TimeSpan.FromHours( 8 );
        return result.SafeValue();
    }

    #endregion

    #region GetAsync

    /// <inheritdoc />
    public async Task<object> GetAsync( string key, Type type, CancellationToken cancellationToken = default ) {
        return await _provider.GetAsync( key, type, cancellationToken );
    }

    /// <inheritdoc />
    public async Task<T> GetAsync<T>( CacheKey key, CancellationToken cancellationToken = default ) {
        key.Validate();
        return await GetAsync<T>( key.Key, cancellationToken );
    }

    /// <inheritdoc />
    public async Task<T> GetAsync<T>( string key, CancellationToken cancellationToken = default ) {
        var result = await _provider.GetAsync<T>( key, cancellationToken );
        return result.Value;
    }

    /// <inheritdoc />
    public async Task<List<T>> GetAsync<T>( IEnumerable<CacheKey> keys, CancellationToken cancellationToken = default ) {
        return await GetAsync<T>( ToKeys( keys ), cancellationToken );
    }

    /// <inheritdoc />
    public async Task<List<T>> GetAsync<T>( IEnumerable<string> keys, CancellationToken cancellationToken = default ) {
        Validate();
        var result = await _cachingProvider.GetAllAsync<T>( keys, cancellationToken );
        return result.Values.Select( t => t.Value ).ToList();
    }

    /// <inheritdoc />
    public async Task<T> GetAsync<T>( CacheKey key, Func<Task<T>> action, CacheOptions options = null, CancellationToken cancellationToken = default ) {
        key.Validate();
        return await GetAsync( key.Key, action, options, cancellationToken );
    }

    /// <inheritdoc />
    public async Task<T> GetAsync<T>( string key, Func<Task<T>> action, CacheOptions options = null, CancellationToken cancellationToken = default ) {
        var result = await _provider.GetAsync( key, action, GetExpiration( options ), cancellationToken );
        return result.Value;
    }

    #endregion

    #region GetByPrefix

    /// <inheritdoc />
    public List<T> GetByPrefix<T>( string prefix ) {
        if( prefix.IsEmpty() )
            return new List<T>();
        Validate();
        return _cachingProvider.GetByPrefix<T>( prefix ).Where( t => t.Value.HasValue ).Select( t => t.Value.Value ).ToList();
    }

    #endregion

    #region GetByPrefixAsync

    /// <inheritdoc />
    public async Task<List<T>> GetByPrefixAsync<T>( string prefix, CancellationToken cancellationToken = default ) {
        if( prefix.IsEmpty() )
            return new List<T>();
        Validate();
        var result = await _cachingProvider.GetByPrefixAsync<T>( prefix, cancellationToken );
        return result.Where( t => t.Value.HasValue ).Select( t => t.Value.Value ).ToList();
    }

    #endregion

    #region TrySet

    /// <inheritdoc />
    public bool TrySet<T>( CacheKey key, T value, CacheOptions options = null ) {
        key.Validate();
        return TrySet( key.Key, value, options );
    }

    /// <inheritdoc />
    public bool TrySet<T>( string key, T value, CacheOptions options = null ) {
        return _provider.TrySet( key, value, GetExpiration( options ) );
    }

    #endregion

    #region TrySetAsync

    /// <inheritdoc />
    public async Task<bool> TrySetAsync<T>( CacheKey key, T value, CacheOptions options = null, CancellationToken cancellationToken = default ) {
        key.Validate();
        return await TrySetAsync( key.Key, value, options, cancellationToken );
    }

    /// <inheritdoc />
    public async Task<bool> TrySetAsync<T>( string key, T value, CacheOptions options = null, CancellationToken cancellationToken = default ) {
        return await _provider.TrySetAsync( key, value, GetExpiration( options ), cancellationToken );
    }

    #endregion

    #region Set

    /// <inheritdoc />
    public void Set<T>( CacheKey key, T value, CacheOptions options = null ) {
        key.Validate();
        Set( key.Key, value, options );
    }

    /// <inheritdoc />
    public void Set<T>( string key, T value, CacheOptions options = null ) {
        _provider.Set( key, value, GetExpiration( options ) );
    }

    /// <inheritdoc />
    public void Set<T>( IDictionary<CacheKey, T> items, CacheOptions options = null ) {
        Set( ToItems( items ), options );
    }

    /// <summary>
    /// 轉換爲緩存項集合
    /// </summary>
    private IDictionary<string, T> ToItems<T>( IDictionary<CacheKey, T> items ) {
        items.CheckNull( nameof( items ) );
        return items.Select( item => {
            item.Key.Validate();
            return new KeyValuePair<string, T>( item.Key.Key, item.Value );
        } ).ToDictionary( t => t.Key, t => t.Value );
    }

    /// <inheritdoc />
    public void Set<T>( IDictionary<string, T> items, CacheOptions options = null ) {
        _provider.SetAll( items, GetExpiration( options ) );
    }

    #endregion

    #region SetAsync

    /// <inheritdoc />
    public async Task SetAsync<T>( CacheKey key, T value, CacheOptions options = null, CancellationToken cancellationToken = default ) {
        key.Validate();
        await SetAsync( key.Key, value, options, cancellationToken );
    }

    /// <inheritdoc />
    public async Task SetAsync<T>( string key, T value, CacheOptions options = null, CancellationToken cancellationToken = default ) {
        await _provider.SetAsync( key, value, GetExpiration( options ), cancellationToken );
    }

    /// <inheritdoc />
    public async Task SetAsync<T>( IDictionary<CacheKey, T> items, CacheOptions options = null, CancellationToken cancellationToken = default ) {
        await SetAsync( ToItems( items ), options, cancellationToken );
    }

    /// <inheritdoc />
    public async Task SetAsync<T>( IDictionary<string, T> items, CacheOptions options = null, CancellationToken cancellationToken = default ) {
        await _provider.SetAllAsync( items, GetExpiration( options ), cancellationToken );
    }

    #endregion

    #region Remove

    /// <inheritdoc />
    public void Remove( CacheKey key ) {
        key.Validate();
        Remove( key.Key );
    }

    /// <inheritdoc />
    public void Remove( string key ) {
        _provider.Remove( key );
    }

    /// <inheritdoc />
    public void Remove( IEnumerable<CacheKey> keys ) {
        Remove( ToKeys( keys ) );
    }

    /// <inheritdoc />
    public void Remove( IEnumerable<string> keys ) {
        _provider.RemoveAll( keys );
    }

    #endregion

    #region RemoveAsync

    /// <inheritdoc />
    public async Task RemoveAsync( CacheKey key, CancellationToken cancellationToken = default ) {
        key.Validate();
        await RemoveAsync( key.Key, cancellationToken );
    }

    /// <inheritdoc />
    public async Task RemoveAsync( string key, CancellationToken cancellationToken = default ) {
        await _provider.RemoveAsync( key, cancellationToken );
    }

    /// <inheritdoc />
    public async Task RemoveAsync( IEnumerable<CacheKey> keys, CancellationToken cancellationToken = default ) {
        await RemoveAsync( ToKeys( keys ), cancellationToken );
    }

    /// <inheritdoc />
    public async Task RemoveAsync( IEnumerable<string> keys, CancellationToken cancellationToken = default ) {
        await _provider.RemoveAllAsync( keys, cancellationToken );
    }

    #endregion

    #region RemoveByPrefix

    /// <summary>
    /// 通過緩存鍵前綴移除緩存
    /// </summary>
    /// <param name="prefix">緩存鍵前綴</param>
    public void RemoveByPrefix( string prefix ) {
        if( prefix.IsEmpty() )
            return;
        _provider.RemoveByPrefix( prefix );
    }

    #endregion

    #region RemoveByPrefixAsync

    /// <inheritdoc />
    public async Task RemoveByPrefixAsync( string prefix, CancellationToken cancellationToken = default ) {
        if( prefix.IsEmpty() )
            return;
        await _provider.RemoveByPrefixAsync( prefix, cancellationToken );
    }

    #endregion

    #region RemoveByPattern

    /// <inheritdoc />
    public void RemoveByPattern( string pattern ) {
        if( pattern.IsEmpty() )
            return;
        _provider.RemoveByPattern( pattern );
    }

    #endregion

    #region RemoveByPatternAsync

    /// <inheritdoc />
    public async Task RemoveByPatternAsync( string pattern, CancellationToken cancellationToken = default ) {
        if( pattern.IsEmpty() )
            return;
        await _provider.RemoveByPatternAsync( pattern, cancellationToken );
    }

    #endregion

    #region Clear

    /// <inheritdoc />
    public void Clear() {
        Validate();
        _cachingProvider.Flush();
    }

    #endregion

    #region ClearAsync

    /// <inheritdoc />
    public async Task ClearAsync( CancellationToken cancellationToken = default ) {
        Validate();
        await _cachingProvider.FlushAsync( cancellationToken );
    }

    #endregion
}

CacheKey 緩存鍵

通過繼承 CacheKey 創建自定義緩存鍵對象,可以封裝緩存鍵的構造細節.

/// <summary>
/// 緩存鍵
/// </summary>
public class CacheKey {
    /// <summary>
    /// 緩存鍵
    /// </summary>
    private string _key;

    /// <summary>
    /// 初始化緩存鍵
    /// </summary>
    public CacheKey() {
    }

    /// <summary>
    /// 初始化緩存鍵
    /// </summary>
    /// <param name="key">緩存鍵</param>
    /// <param name="parameters">緩存鍵參數</param>
    public CacheKey( string key,params object[] parameters) {
        _key = string.Format( key, parameters );
    }

    /// <summary>
    /// 緩存鍵
    /// </summary>
    public string Key {
        get => ToString();
        set => _key = value;
    }

    /// <summary>
    /// 緩存鍵前綴
    /// </summary>
    public string Prefix { get; set; }

    /// <summary>
    /// 獲取緩存鍵
    /// </summary>
    public override string ToString() {
        return $"{Prefix}{_key}";
    }
}

CacheAttribute 緩存攔截器

[Cache] 緩存攔截器提供了緩存操作的快捷方式.

/// <summary>
/// 緩存攔截器
/// </summary>
public class CacheAttribute : InterceptorBase {
    /// <summary>
    /// 緩存鍵前綴,可使用佔位符, {0} 表示第一個參數值,範例: User-{0}
    /// </summary>
    public string Prefix { get; set; }
    /// <summary>
    /// 緩存過期間隔,單位:秒,默認值:36000
    /// </summary>
    public int Expiration { get; set; } = 36000;

    /// <summary>
    /// 執行
    /// </summary>
    public override async Task Invoke( AspectContext context, AspectDelegate next ) {
        var cache = GetCache( context );
        var returnType = GetReturnType( context );
        var key = CreateCacheKey( context );
        var value = await GetCacheValue( cache, returnType, key );
        if( value != null ) {
            SetReturnValue( context, returnType, value );
            return;
        }
        await next( context );
        await SetCache( context, cache, key );
    }

    /// <summary>
    /// 獲取緩存服務
    /// </summary>
    protected virtual ICache GetCache( AspectContext context ) {
        return context.ServiceProvider.GetService<ICache>();
    }

    /// <summary>
    /// 獲取返回類型
    /// </summary>
    private Type GetReturnType( AspectContext context ) {
        return context.IsAsync() ? context.ServiceMethod.ReturnType.GetGenericArguments().First() : context.ServiceMethod.ReturnType;
    }

    /// <summary>
    /// 創建緩存鍵
    /// </summary>
    private string CreateCacheKey( AspectContext context ) {
        var keyGenerator = context.ServiceProvider.GetService<ICacheKeyGenerator>();
        return keyGenerator.CreateCacheKey( context.ServiceMethod, context.Parameters, GetPrefix( context ) );
    }

    /// <summary>
    /// 獲取緩存鍵前綴
    /// </summary>
    private string GetPrefix( AspectContext context ) {
        try {
            return string.Format( Prefix, context.Parameters.ToArray() );
        }
        catch {
            return Prefix;
        }
    }

    /// <summary>
    /// 獲取緩存值
    /// </summary>
    private async Task<object> GetCacheValue( ICache cache, Type returnType, string key ) {
        return await cache.GetAsync( key, returnType );
    }

    /// <summary>
    /// 設置返回值
    /// </summary>
    private void SetReturnValue( AspectContext context, Type returnType, object value ) {
        if( context.IsAsync() ) {
            context.ReturnValue = typeof( Task ).GetMethods()
                .First( p => p.Name == "FromResult" && p.ContainsGenericParameters )
                .MakeGenericMethod( returnType ).Invoke( null, new[] { value } );
            return;
        }
        context.ReturnValue = value;
    }

    /// <summary>
    /// 設置緩存
    /// </summary>
    private async Task SetCache( AspectContext context, ICache cache, string key ) {
        var options = new CacheOptions { Expiration = TimeSpan.FromSeconds( Expiration ) };
        var returnValue = context.IsAsync() ? await context.UnwrapAsyncReturnValue() : context.ReturnValue;
        await cache.SetAsync( key, returnValue, options );
    }
}

LocalCacheAttribute 本地緩存攔截器

/// <summary>
/// 本地緩存攔截器
/// </summary>
public class LocalCacheAttribute : CacheAttribute {
    /// <summary>
    /// 獲取緩存服務
    /// </summary>
    protected override ICache GetCache( AspectContext context ) {
        return context.ServiceProvider.GetService<ILocalCache>();
    }
}

RedisCacheAttribute Redis緩存攔截器

/// <summary>
/// Redis緩存攔截器
/// </summary>
public class RedisCacheAttribute : CacheAttribute {
    /// <summary>
    /// 獲取緩存服務
    /// </summary>
    protected override ICache GetCache( AspectContext context ) {
        return context.ServiceProvider.GetService<IRedisCache>();
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章