Util應用框架基礎(七) - 快取

何鎮汐發表於2023-11-21

本節介紹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>();
    }
}

相關文章