MasaFramework -- 快取入門與設計

2022-10-21 18:02:09

概念

什麼是快取,在專案中,為了提高資料的讀取速度,我們會對不經常變更但存取頻繁的資料做快取處理,我們常用的快取有:

功能

目前,MasaFramework為我們提供了以下能力

入門

分散式快取

  1. 新建ASP.NET Core 空專案Assignment.DistributedCache,並安裝Masa.Contrib.Caching.Distributed.StackExchangeRedis
dotnet new web -o Assignment.DistributedCache
cd Assignment.DistributedCache

dotnet add package Masa.Contrib.Caching.Distributed.StackExchangeRedis --version 0.6.0-rc.5
  1. 設定Redis設定資訊
{
    "RedisConfig":{
        "Servers":[
            {
                "Host":"localhost",
                "Port":6379
            }
        ],
        "DefaultDatabase":3,
        "ConnectionPoolSize":10
    }
}
  1. 註冊分散式快取,並使用Redis快取,修改Program.cs
var builder = WebApplication.CreateBuilder(args);

//註冊分散式快取
builder.Services.AddDistributedCache(distributedCacheOptions =>
{
    distributedCacheOptions.UseStackExchangeRedisCache();//使用分散式Redis快取, 預設使用本地`RedisConfig`下的設定
});

使用分散式快取的資料來源預設為 IOptionsMonitor<RedisConfigurationOptions>,如果本地未正確在RedisConfig節點設定快取資訊,且專案中也沒有通過其它方式設定使其支援選項模式,則預設使用的Redis設定為: 地址: localhost、埠:6379,密碼:空,資料庫:db0

  1. 新建User類,用於接收使用者資訊
public class User
{
    public string Name { get; set; }

    public int Age { get; set; }
}
  1. 如何使用IDistributedCacheClient,修改Program.cs
// 設定快取
app.MapPost("/set/{id}", async (IDistributedCacheClient distributedCacheClient, [FromRoute] string id, [FromBody] User user) =>
{
    await distributedCacheClient.SetAsync(id, user);
    return Results.Accepted();
});

// 獲取快取
app.MapGet("/get/{id}", async (IDistributedCacheClient distributedCacheClient, [FromRoute] string id) =>
{
    var value = await distributedCacheClient.GetAsync<User>(id);
    return Results.Ok(value);
});

多級快取

  1. 新建ASP.NET Core 空專案Assignment.DistributedCache,並安裝Masa.Contrib.Caching.MultilevelCacheMasa.Contrib.Caching.Distributed.StackExchangeRedis
dotnet new web -o Assignment.MultilevelCache
cd Assignment.MultilevelCache

dotnet add package Masa.Contrib.Caching.MultilevelCache --version 0.6.0-rc.5
dotnet add package Masa.Contrib.Caching.Distributed.StackExchangeRedis --version 0.6.0-rc.5
  1. 註冊多級快取,並使用分散式Redis快取,修改Program.cs
var builder = WebApplication.CreateBuilder(args);

//註冊多級快取
builder.Services.AddMultilevelCache(distributedCacheOptions =>
{
    distributedCacheOptions.UseStackExchangeRedisCache();//使用分散式Redis快取
});
  1. 新建User類,用於接收使用者資訊
public class User
{
    public string Name { get; set; }

    public int Age { get; set; }
}
  1. 如何使用IMultilevelCacheClient,修改Program.cs
// 設定快取
app.MapPost("/set/{id}", async (IMultilevelCacheClient multilevelCacheClient, [FromRoute] string id, [FromBody] User user) =>
{
    await multilevelCacheClient.SetAsync(id, user);
    return Results.Accepted();
});

// 獲取快取
app.MapGet("/get/{id}", async (IMultilevelCacheClient multilevelCacheClient, [FromRoute] string id) =>
{
    var value = await multilevelCacheClient.GetAsync<User>(id);
    return Results.Ok(value);
});

測試

藉助Postman或者Swagger或者使用其它API測試工具,分別測試設定快取與獲取快取,以驗證分散式快取以及多級快取是可以正常使用的。

友情提示:檢查Redis快取,找到剛剛你設定的快取,確定下它的儲存結果是否與你想象的一致!!

規則

經過測試,我們的分散式快取與多級快取是可以正常使用的,但檢視Redis的儲存結果後,發現它們實際的儲存與我們心目中的結果好像是有點出入,它們分別是:

  1. 快取Key不同 (與我們設定的Key不完全一致)
  2. 結構不同 (實際儲存的為Hash型別)
  3. 內容不同 (內容經過壓縮)

快取Key的生成規則

快取Key支援三種規則:

列舉 描述
None 1 不做處理,傳入的Key即為實際的快取Key
TypeName 2 實際的快取Key = $"{GetTypeName(T)}.{傳入快取Key}" (預設)
TypeAlias 3 根據TypeName得到對應的別名與Key的組合,Format: ${TypeAliasName}{:}{key}

詳細規則可檢視

儲存結構與規則

Masa.Contrib.Caching.Distributed.StackExchangeRedis使用的是Hash儲存,通過使用Hash儲存,支援快取的絕對過期以及相對過期,其中:

描述 詳細 特殊
absexp 絕對過期時間的Ticks 自公曆 0001-01-01 00:00:00:000 到絕對過期時間的計時週期數 (1週期 = 100ns 即 1/10000 ms) -1 為永不過期
sldexp 滑動過期時間的Ticks 自公曆 0001-01-01 00:00:00:000 到滑動過期時間的計時週期數 (1週期 = 100ns 即 1/10000 ms,每次獲取資料時會重新整理滑動過期時間) -1 為永不過期
data 資料 儲存使用者設定的快取資料

內容壓縮規則

  1. 當儲存值型別為以下型別時,不對資料進行壓縮:
  • Byte
  • SByte
  • UInt16
  • UInt32
  • UInt64
  • Int16
  • Int32
  • Int64
  • Double
  • Single
  • Decimal
  1. 當儲存值型別為字串時,對資料進行壓縮
  2. 當儲存值型別不滿足以上條件時,對資料進行序列化並進行壓縮

分散式Redis快取範例

分散式快取註冊

方案一. 通過本地組態檔註冊

  1. 修改appsettings.json檔案
{
    "RedisConfig":{
        "Servers":[
            {
                "Host":"localhost",
                "Port":6379
            }
        ],
        "DefaultDatabase":3,
        "ConnectionPoolSize":10
    }
}
  1. 註冊分散式Redis快取
builder.Services.AddDistributedCache(distributedCacheOptions =>
{
    distributedCacheOptions.UseStackExchangeRedisCache();
});

方案二. 手動指定Redis設定註冊

builder.Services.AddDistributedCache(distributedCacheOptions =>
{
    distributedCacheOptions.UseStackExchangeRedisCache(options =>
    {
        options.Servers = new List<RedisServerOptions>()
        {
            new("localhost", 6379)
        };
        options.DefaultDatabase = 3;
        options.ConnectionPoolSize = 10;
        options.GlobalCacheOptions = new CacheOptions()
        {
            CacheKeyType = CacheKeyType.None //全域性禁用快取Key格式化處理
        };
    });
});

方案三. 通過選項模式註冊

  1. 通過Configure方法使其支援選項模式
builder.Services.Configure<RedisConfigurationOptions>(redisConfigurationOptions =>
{
    redisConfigurationOptions.Servers = new List<RedisServerOptions>()
    {
        new("localhost", 6379)
    };
    redisConfigurationOptions.DefaultDatabase = 3;
    redisConfigurationOptions.ConnectionPoolSize = 10;
    redisConfigurationOptions.GlobalCacheOptions = new CacheOptions()
    {
        CacheKeyType = CacheKeyType.None
    };
});
  1. 註冊分散式Redis快取
builder.Services.AddDistributedCache(distributedCacheOptions =>
{
    distributedCacheOptions.UseStackExchangeRedisCache();
});

方案四. 通過指定Configuration註冊

  1. 在Redis快取的設定儲存到本地appsettings.json檔案
{
    "RedisConfig":{
        "Servers":[
            {
                "Host": "localhost",
                "Port": 6379
            }
        ],
        "DefaultDatabase": 3,
        "ConnectionPoolSize": 10
    }
}
  1. 指定Configuration註冊分散式Redis快取
var builder = WebApplication.CreateBuilder(args);

//註冊分散式快取
builder.Services.AddDistributedCache(distributedCacheOptions =>
{
    // 使用儲存Redis設定的Configuration
    distributedCacheOptions.UseStackExchangeRedisCache(builder.Configuration.GetSection("RedisConfig"));
});

方案五. 將設定儲存到Dcc上,並通過Configuration提供的手動對映功能,實現選項模式

  1. 使用Dcc,並手動指定對映
builder.AddMasaConfiguration(configurationBuilder =>
{
    configurationBuilder.UseDcc();//使用Dcc 擴充套件Configuration能力,支援遠端設定

    configurationBuilder.UseMasaOptions(options =>
    {
        //通過手動對映RedisConfigurationOptions的設定,實現選項模式
        options.MappingConfigurationApi<RedisConfigurationOptions>("{替換為Dcc中設定所屬的AppId}", "{替換為Redis設定的物件名稱}");
    });
});
  1. 註冊分散式Redis快取
builder.Services.AddDistributedCache(distributedCacheOptions =>
{
    distributedCacheOptions.UseStackExchangeRedisCache();
});

方案三、四、五的本質都是通過支援選項模式來註冊分散式Redis快取

修改快取Key對映規則

修改快取Key對映規則十分簡單,我們在設定時更改CacheKeyType為對應的規則即可,但當 CacheKeyType = 3 需要注意,它需要額外提供型別名與別名的對應關係,完整例子如下:

  1. 修改appsettings.json, 將CacheKeyType的值改為 3
{
    "RedisConfig":{
        "Servers":[
            {
                "Host":"localhost",
                "Port":6379
            }
        ],
        "DefaultDatabase":3,
        "ConnectionPoolSize":10,
        "GlobalCacheOptions": {
          "CacheKeyType": 3 //CacheKeyType為3時啟用別名格式化快取Key,可節省快取Key的鍵長度
        }
    }
}
  1. 註冊分散式快取並設定型別名與別名的對應關係
builder.Services.AddDistributedCache(distributedCacheOptions =>
{
    distributedCacheOptions.UseStackExchangeRedisCache();
}, typeAliasOptions =>
{
    typeAliasOptions.GetAllTypeAliasFunc = () => new Dictionary<string, string>()
    {
        { "String", "s" }//當型別為String時,格式化後的Key為 s:key
    };
});

通過指定型別與別名的對應關係,從而使得最終形成較短的快取Key,以達到節省儲存空間的目的,快取Key生成規則可檢視

多級快取範例

多級快取註冊

方案一. 通過本地組態檔註冊

  1. 修改appsettings.json檔案,分別設定多級快取設定以及Redis快取設定
{
  // 多級快取全域性設定,非必填
  "MultilevelCache": {
    "SubscribeKeyPrefix": "masa",//預設訂閱方key字首,用於拼接channel
    "SubscribeKeyType": 3, //預設訂閱方key的型別,預設ValueTypeFullNameAndKey,用於拼接channel
    "CacheEntryOptions": {
      "AbsoluteExpirationRelativeToNow": "00:00:30",//絕對過期時長(距當前時間)
      "SlidingExpiration": "00:00:50"//滑動過期時長(距當前時間)
    }
  },

  // Redis分散式快取設定
  "RedisConfig": {
    "Servers": [
      {
        "Host": "localhost",
        "Port": 6379
      }
    ],
    "DefaultDatabase": 3
  }
}
  1. 新增多級快取並使用分散式Redis快取
builder.Services.AddMultilevelCache(distributedCacheOptions =>
{
    distributedCacheOptions.UseStackExchangeRedisCache();
});

方案二. 通過手動指定設定

builder.Services.AddMultilevelCache(distributedCacheOptions =>
{
    distributedCacheOptions.UseStackExchangeRedisCache(RedisConfigurationOptions);
});

未設定記憶體快取時,預設記憶體快取永久有效

除了上述兩種方式以外,多級快取的記憶體快取設定也同樣支援選項模式,我們可以通過Dcc或者利用 builder.Services.Configure<MultilevelCacheOptions>(builder.Configuration)來支援選項模式

修改快取Key對映規則

原始碼解讀

IDistributedCacheClient (分散式快取使用者端)

IDistributedCacheClient介面提供以下方法來處理分散式快取

以下方法會根據全域性快取Key的規則設定以及傳入快取Key的規則設定,檢測是否需要格式化快取Key,對需要格式化Key的操作按照快取Key格式化規則進行處理,詳細檢視:

  • Get<T>GetAsync<T>: 根據快取Key返回型別為T的結果 (如果快取不存在,則返回Null)
  • GetList<T>GetListAsync<T>: 根據快取Key集合返回對應的快取值的集合 (針對不存在的快取key,其值返回Null)
  • GetOrSet<T>GetOrSetAsync<T>: 如果在快取中找到,則返回型別為T的結果,如果快取未找到,則執行Setter,並返回Setter的結果
  • Set<T>SetAsync<T>: 將指定的快取Key以及快取值新增到快取
  • SetList<T>SetListAsync<T>: 將指定的快取Key、Value集合新增快取
  • Remove<T>RemoveAsync<T>: 將指定的快取Key (快取Key集合) 從快取中移除
  • Refresh<T>RefreshAsync<T>: 重新整理指定的快取Key (快取Key集合) 的生命週期
    • 適用於未被刪除、絕對過期時間沒有到,但相對過期時間快到的快取 (延長滑動過期時間)
  • Exists<T>ExistsAsync<T>: 如果在快取中找到,則返回true,否則返回false
  • GetKeys<T>GetKeysAsync<T>: 根據key pattern 得到符合規則的所有快取Key
  • GetByKeyPattern<T>GetByKeyPatternAsync<T>: 根據key pattern 得到符合規則的所有快取Key、Value集合
  • HashIncrementAsync: 將指定的快取Key的值增加Value,並返回增長後的結果
  • HashDecrementAsync: 將指定的快取Key的值減少Value,並返回減少後的結果
    • 支援設定最小的Value,避免減少後的值低於設定的最小值,執行失敗則返回: -1
  • KeyExpire<T>KeyExpireAsync<T>: 設定快取Key的生命週期

以下方法不執行快取Key格式化, 應傳入快取完整Key:

  • RemoveRemoveAsync: 將指定的快取Key (快取Key集合) 從快取中移除
  • RefreshRefreshAsync: 重新整理指定的快取Key (快取Key集合) 的生命週期
    • 適用於未被刪除、絕對過期時間沒有到,但相對過期時間快到的快取
  • ExistsExistsAsync: 如果在快取中找到,則返回true,否則返回false
  • GetKeysGetKeysAsync: 根據key pattern 得到符合規則的所有快取Key
    • 例: 傳入User*,可得到快取中以User開頭的所有快取Key
  • KeyExpireKeyExpireAsync: 設定快取Key的生命週期

IMultilevelCacheClient (多級快取使用者端)

  • Get<T>GetAsync<T>: 根據快取Key返回型別為T的結果 (如果快取不存在,則返回Null) (支援監控快取變更)
  • GetList<T>GetListAsync<T>: 根據快取Key集合返回對應的快取值的集合 (針對不存在的快取key,其值返回Null)
  • GetOrSet<T>GetOrSetAsync<T>: 如果在快取中找到,則返回型別為T的結果,如果快取未找到,則執行Setter,並返回Setter的結果
  • Set<T>SetAsync<T>: 將指定的快取Key以及快取值新增到快取
  • SetList<T>SetListAsync<T>: 將指定的快取Key、Value集合新增快取
  • Remove<T>RemoveAsync<T>: 將指定的快取Key (快取Key集合) 從快取中移除
  • Refresh<T>RefreshAsync<T>: 重新整理指定的快取Key (快取Key集合) 的生命週期
    • 適用於未被刪除、絕對過期時間沒有到,但相對過期時間快到的快取 (延長滑動過期時間)

IDistributedCacheClientFactory (分散式快取工廠)

  • Create: 返回指定Name的分散式快取使用者端

IMultilevelCacheClientFactory (多級快取工廠)

  • Create: 返回指定Name的多級快取使用者端

如果Name為空字串時,可直接使用IDistributedCacheClientIMultilevelCacheClient, 預設註冊不指定Name時,則其Name為空字串,可不通過Factory建立

總結

Masa Framework提供了分散式快取以及多級快取的實現,其中有幾個優秀的功能:

  • 多級快取提供了快取更新後同步更新記憶體快取功能
    • 當我們的服務是多副本時,不必擔心會快取更新後其它副本由於記憶體快取未過期,導致獲取到過期的快取資料,大大提升我們的使用者體驗
  • 支援滑動過期以及絕對過期混合使用
    • 避免無用的快取長時間被持久化,但對於熱點資料又可以避免打到Redis或者資料庫
  • 設定支援熱更新,設定更新後同步生效,無需重啟專案
  • 快取Key支援格式化,可根據當前快取值型別與傳入快取Key結合形成新的快取Key,提高了開發效率以及程式碼可讀性
    • 比如獲取使用者id為1的資料,可通過Client.Get<User>("1"),而無需:Client.Get<User>("User.1")

本章原始碼

Assignment16

https://github.com/zhenlei520/MasaFramework.Practice

開源地址

MASA.Framework:https://github.com/masastack/MASA.Framework

MASA.EShop:https://github.com/masalabs/MASA.EShop

MASA.Blazor:https://github.com/BlazorComponent/MASA.Blazor

如果你對我們的 MASA Framework 感興趣,無論是程式碼貢獻、使用、提 Issue,歡迎聯絡我們