Masa Framework原始碼解讀-02快取模組(分散式快取進階之多級快取)

隔壁老黎發表於2023-03-16

序言

​ 今天這篇文章來看看Masa Framework的快取設計,上一篇文章中說到的MasaFactory的應用也會在這章節出現。文章中如有錯誤之處還請指點,我們們話不多說,直入主題。

Masa Framework快取簡介

MASA Framework原始碼地址:https://github.com/masastack/MASA.Framework

​ Masa Framework中的快取元件支援 分散式快取分散式多級快取 (PS:Masa Framework的快取元件並不與框架強行繫結,也就是說我們可以在自己的框架中使用masa framework的快取元件,而不是我必須用masa framework才能使用它的快取元件,這一點必須給官方點個大大的贊)。首先分散式快取大家多多少少都聽說過也用過,在這不多介紹分散式快取的概念。我們來看下多級快取吧,其實多級快取這個概念很早就有,但是在.net中沒怎麼看到這個設計的落地實現框架,今天剛好藉著解讀masa framework的原始碼,我們來看下多級快取設計。

多級快取的定義

什麼是多級快取?既然已經有了分散式快取,為什麼還要多級快取?

​ 首先什麼是多級快取?多級快取是指在一個系統的不同架構層級進行資料快取,以提升訪問效率。其次有了分散式快取,為什麼還要多級快取?是因為在讀取資料頻率很高的情況下,分散式快取面臨著兩個問題:響應速度和高可用。響應速度問題是指當訪問層發一起一個網路請求到分散式快取處理完請求返回的時間,是需要一個過程,而網路請求的不確定性以及耗時時長是不可避免的。而高可用問題是指大量讀取資料請求過來讀取快取的時候,分散式快取能否扛得住這麼大的壓力,當然這個有解決方案可以使用叢集解決,但是叢集之後會有資料一致性問題並且它讀取資料還是得走網路通訊。

​ 而多級快取就是為了最佳化分散式快取存在的一些問題,而衍生另一種手段。所謂多級快取可以簡單理解為是透過在分散式快取和我們的訪問層中間在增加若干層快取來減少對分散式快取的網路請求和分散式快取的壓力。(PS:多級快取存在的資料一致性問題,這個Masa Framework已經幫我們解決了

MASA Framework中的多級快取設計

首先Masa Framework有一套分散式快取介面及實現,而Masa Framework的多級快取是在分散式快取的基礎上,在加了一層記憶體快取。而多級快取資料的一致性問題,masa framework是透過redis的pub、sub釋出訂閱解決的(PS:這塊的釋出訂閱官方有一個抽象,並不直接依賴redis,請往下看)。

  • 當訪問層讀取快取資料時,先從記憶體裡面獲取下,如果沒有則向分散式快取獲取並寫入到記憶體快取,並且同時開啟一個關於快取key的分散式訂閱,如果收到訊息則同步更新記憶體快取。

  • 當訪問層寫入快取時,同時寫入記憶體以及分散式快取,然後再發布關於快取key的分散式訊息,其它客戶端收到訊息時則同步更新各自記憶體快取資料。

原始碼解讀

接下來讓我們來看下Masa Framework的原始碼設計,首先我們把原始碼下載下來,然後開啟。下載地址:https://github.com/masastack/MASA.Framework

原始碼目錄結構

  • masa framework快取元件分為兩部分,一個是BuildingBlocks下的Caching抽象介面,另外一個是Contrib下的Caching介面實現。結構如下圖:

程式碼設計

Masa Framework整個快取元件分為三個類庫專案,分別是:Masa.BuildingBlocks.CachingMasa.Contrib.Caching.Distributed.StackExchangeRedisMasa.Contrib.Caching.MultilevelCache

​ 首先Masa.BuildingBlocks.Caching這個類庫就是將我們經常用到的快取方法抽象了一層(IDistributedCacheClient、IMultilevelCacheClient),其中包含分散式快取以及多級快取常用的方法,如:Get、Set、Refresh、Remove,分散式快取中的(Subscribe、Publish等)。

​ 而Masa.Contrib.Caching.Distributed.StackExchangeRedis 這個類庫實現了分散式快取(PS:這個庫沒有實現多級快取IMultilevelCacheClient介面,個人覺得其實應該將Masa.BuildingBlocks.Caching這個類庫再拆分出兩個包,將分散式和多級快取分開)。

​ 最後Masa.Contrib.Caching.MultilevelCache這個類庫實現了多級快取(這個類庫沒有實現分散式快取IDistributedCacheClient介面,但是多級快取依賴了IDistributedCacheClient)。最終整個快取的設計如下圖所示:

  • Masa.BuildingBlocks.Caching :這個類庫包含了分散式快取和多級快取的抽象介面以及抽象基類

    • ICacheClient:快取公共方法抽象(把多級快取和分散式快取都有的方法在封裝一層,如:Get、Set、Refersh等方法)
    • CacheClientBase :快取抽象基類,對方法進行封裝(比如Get、GetList,最終都呼叫GetList方法等)
    • IDistributedCacheClient :分散式快取介面抽象(Get、Set、Refersh、Publish、Subscribe等方法),繼承ICacheClient
    • DistributedCacheClientBase :分散式快取抽象基類,對方法進行封裝(比如Get、GetList,最終都呼叫GetList方法等)
    • IMultilevelCacheClient :多級快取介面抽象(Get、Set、Refersh等方法),繼承ICacheClient
    • MultilevelCacheClientBase :多級快取抽象基類,對方法進行封裝(比如Get、GetList,最終都呼叫GetList方法等)
    • 構建工廠
      • ICacheClientFactory<TService> :快取工廠抽象,繼承自IMasaFactory<TService> 構建工廠
      • CacheClientFactoryBase<TService> :快取工廠抽象基類,繼承自MasaFactoryBase<TService>
      • IDistributedCacheClientFactory :用於建立分散式快取IDistributedCacheClient 介面,繼承自ICacheClientFactory<IDistributedCacheClient>
      • DistributedCacheClientFactoryBase :分散式快取建立工廠實現類,建立IDistributedCacheClient 介面例項。
      • IMultilevelCacheClientFactory :用於建立多級快取IMultilevelCacheClient 介面,繼承自ICacheClientFactory<IMultilevelCacheClient>
      • MultilevelCacheClientFactoryBase :多級快取建立工廠實現類,建立IMultilevelCacheClient 介面例項。
  • Masa.Contrib.Caching.Distributed.StackExchangeRedis : 分散式快取IDistributedCacheClient介面的實現

    • RedisCacheClientBase :redis實現分散式快取介面,進行再一步封裝,將redis連線、訂閱、配置等初始化。繼承DistributedCacheClientBase
    • RedisCacheClient :分散式快取的redis實現,繼承RedisCacheClientBase
  • Masa.Contrib.Caching.MultilevelCache :多級快取實現

    • MultilevelCacheClient :多級快取實現,內部依賴IDistributedCacheClient

整個快取元件的設計,最主要類是這些,當然還有一些option配置和幫助類,我就沒有畫出來,這個留待大家自己去探索

Demo案例

Demo案例專案地址:https://github.com/MapleWithoutWords/masa-demos/tree/main/src/CachingDemo

上面也說到Masa Framework的快取元件不與框架強繫結,也就是說我們可以在自己的框架中使用masa的快取元件,下面我將展示兩個專案,它們分別使用分散式快取和多級快取。

分散式快取使用

  1. 第一步,在我們的專案中安裝分散式快取元件Masa.Contrib.Caching.Distributed.StackExchangeRedis (下載1.0.0-preview.18版本以上),或在專案目錄下使用命令列安裝
dotnet add package Masa.Contrib.Caching.Distributed.StackExchangeRedis --version 1.0.0-preview.18
  1. 第二步,在Program.cs檔案中新增以下程式碼
builder.Services.AddDistributedCache(opt =>
{
    opt.UseStackExchangeRedisCache();
});
  1. 第三步,在配置檔案中增加以下配置。這邊再補充以下,masa的redis分散式快取是支援叢集的,只需要在Servers下配置多個節點就行
"RedisOptions": {
  "Servers": [
    {
      "Host": "127.0.0.1",
      "Port": "6391"
    }
  ],
  "DefaultDatabase": 0,
  "Password": "123456"
}
  1. 第四步:在建構函式中注入 IDistributedCacheClient 或者 IDistributedCacheClientFactory 物件,其實直接注入的IDistributedCacheClient 也是由IDistributedCacheClientFactory 建立之後,注入到容器中的單例物件。

    • 建構函式中注入 IDistributedCacheClient :這個注入的物件生命週期為單例,也就是說從容器中獲取的始終是同一個物件
        public class DistributedCacheClientController : ControllerBase
        {
            private static readonly string[] Summaries = new[] { "Data1", "Data2", "Data3" };
            private readonly IDistributedCacheClient _distributedCacheClient;
            public DistributedCacheClientController(IDistributedCacheClient distributedCacheClient) => _distributedCacheClient = distributedCacheClient;
    
            [HttpGet]
            public async Task<IEnumerable<string>> Get()
            {
                var cacheList = await _distributedCacheClient.GetAsync<string[]>(nameof(Summaries));
                if (cacheList != null)
                {
                    Console.WriteLine($"從快取中獲取資料:【{string.Join(",", cacheList)}】");
                    return cacheList;
                }
                Console.WriteLine($"寫入資料到快取");
                await _distributedCacheClient.SetAsync(nameof(Summaries), Summaries);
                return Summaries;
            }
        }
    
    • 使用IDistributedCacheClientFactory :使用工廠建立的每一個物件都是一個新的例項,需要手動管理物件生命週期,比如不使用之後要dispose。擴充套件:這塊還可以使用自己實現的IDistributedCacheClient例項去操作,不太理解的可以看下我上篇文章 。不過建議直接注入IDistributedCacheClient 使用,不太推薦工廠,除非你有場景需要用到一個新的例項。
    public class DistributedCacheClientFactoryController : ControllerBase
    {
        private static readonly string[] FactorySummaries = new[] { "FactoryData1", "FactoryData2", "FactoryData3" };
        private readonly IDistributedCacheClientFactory _distributedCacheClientFactory;
        public DistributedCacheClientFactoryController(IDistributedCacheClientFactory distributedCacheClientFactory) => _distributedCacheClientFactory = distributedCacheClientFactory;
    
        [HttpGet]
        public async Task<IEnumerable<string>> GetByFactory()
        {
            using (var distributedCacheClient = _distributedCacheClientFactory.Create())
            {
                var cacheList = await distributedCacheClient.GetAsync<string[]>(nameof(FactorySummaries));
                if (cacheList != null)
                {
                    Console.WriteLine($"使用工廠從快取中獲取資料:【{string.Join(",", cacheList)}】");
                    return cacheList;
                }
                Console.WriteLine($"使用工廠寫入資料到快取");
                await distributedCacheClient.SetAsync(nameof(FactorySummaries), FactorySummaries);
                return FactorySummaries;
            }
        }
    }
    
  2. 最終結果:注:記得啟動本地redis

多級快取使用

  1. 第一步,在我們的專案中安裝元件: Masa.Contrib.Caching.MultilevelCacheMasa.Contrib.Caching.Distributed.StackExchangeRedis (下載1.0.0-preview.18版本以上),或在專案目錄下使用命令列安裝
dotnet add package Masa.Contrib.Caching.MultilevelCache --version 1.0.0-preview.18
dotnet add package Masa.Contrib.Caching.Distributed.StackExchangeRedis --version 1.0.0-preview.18
  1. 第二步,在Program.cs檔案中新增以下程式碼
builder.Services.AddMultilevelCache(opt =>
{
    opt.UseStackExchangeRedisCache();
});
  1. 第三步,在配置檔案中增加以下配置。多級快取依賴於分散式快取,所以需要新增redis配置,如下:
"RedisOptions": {
  "Servers": [
    {
      "Host": "127.0.0.1",
      "Port": "6391"
    }
  ],
  "DefaultDatabase": 0,
  "Password": "123456"
}
  1. 第四步:在建構函式中注入 IMultilevelCacheClient 或者 IMultilevelCacheClientFactory 物件。

    • 建構函式中注入 IMultilevelCacheClient :這個注入的物件生命週期為單例,也就是說從容器中獲取的始終是同一個物件
    public class MultilevelCacheClientController : ControllerBase
    {
        private readonly IMultilevelCacheClient _multilevelCacheClient;
        public MultilevelCacheClientController(IMultilevelCacheClient multilevelCacheClient) => _multilevelCacheClient = multilevelCacheClient;
    
        [HttpGet]
        public async Task<string> GetAsync()
        {
            var key = "MultilevelCacheFactoryTest";
            var cacheValue = await _multilevelCacheClient.GetAsync<string>(key);
            if (cacheValue != null)
            {
                Console.WriteLine($"get data by multilevel cahce:【{cacheValue}】");
                return cacheValue;
            }
            cacheValue = value;
            Console.WriteLine($"write data【{cacheValue}】to multilevel cache");
            await _multilevelCacheClient.SetAsync(key, cacheValue);
            return cacheValue;
        }
    }
    
    • 使用IDistributedCacheClientFactory :使用工廠建立的每一個物件都是一個新的例項,需要手動管理物件生命週期,比如不使用之後要dispose。建議直接注入IDistributedCacheClient 使用,不太推薦工廠,除非你有場景需要用到一個新的例項。
    public class MultilevelCacheClientController : ControllerBase
    {
        const string key = "MultilevelCacheTest";
        private readonly IMultilevelCacheClient _multilevelCacheClient;
        public MultilevelCacheClientController(IMultilevelCacheClient multilevelCacheClient) => _multilevelCacheClient = multilevelCacheClient;
    
        [HttpGet]
        public async Task<string?> GetAsync()
        {
            var cacheValue = await _multilevelCacheClient.GetAsync<string>(key, val => { Console.WriteLine($"值被改變了:{val}"); }, null);
            if (cacheValue != null)
            {
                Console.WriteLine($"get data by multilevel cahce:【{cacheValue}】");
                return cacheValue;
            }
            cacheValue = "multilevelClient";
            Console.WriteLine($"use factory write data【{cacheValue}】to multilevel cache");
            await _multilevelCacheClient.SetAsync(key, cacheValue);
            return cacheValue;
        }
    
        [HttpPost]
        public async Task<string?> SetAsync(string value = "multilevelClient")
        {
            Console.WriteLine($"use factory write data【{value}】to multilevel cache");
            await _multilevelCacheClient.SetAsync(key, value);
            return value;
        }
    
        [HttpDelete]
        public async Task RemoveAsync()
        {
            await _multilevelCacheClient.RemoveAsync<string>(key);
        }
    }
    
  2. 執行程式

    • 我這邊啟動以命令列啟動了兩個服務模擬不同服務或者叢集

      dotnet run --urls=http://*:2001
      dotnet run --urls=http://*:2002
      
    • 在埠2001的程式寫入資料之後,埠2002的程式能夠讀取到資料

    • 在埠2001的程式修改 快取資料 ,埠2002的程式能夠同步新的快取資料過來

總結

其實任何語言都能實現這個多級快取功能,我們去看框架原始碼,不僅是對功能原理的探索,也是學習別人的設計思想。

  • 提升訪問速度,降低分散式快取壓力:masa的多級快取優先從記憶體讀取資料,提高程式訪問速度。間接減少網路請求,降低了分散式快取的壓力。

  • 快取高度擴充套件性:MASA Framework的快取元件可以支援自己去實現自己的快取邏輯,比如說目前masa的分散式快取使用redis,我想用其它的快取元件,或者我覺得masa實現的不優雅,完全可以自己定製。

相關文章