Masa Framework源码解读-02缓存模块(分布式缓存进阶之多级缓存)

发布时间 2023-03-22 19:13:59作者: 隔壁老黎

序言

​ 今天这篇文章来看看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
    {
        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);
        }
    }
    
    • 使用IMultilevelCacheClientFactory :使用工厂创建的每一个对象都是一个新的实例,需要手动管理对象生命周期,比如不使用之后要dispose。建议直接注入IMultilevelCacheClient 使用,不太推荐工厂,除非你有场景需要用到一个新的实例。
        public class MultilevelCacheClientFactoryController : ControllerBase
        {
            const string key = "MultilevelCacheFactoryTest";
            private readonly IMultilevelCacheClientFactory _multilevelCacheClientFactory;
            public MultilevelCacheClientFactoryController(IMultilevelCacheClientFactory multilevelCacheClientFactory) => _multilevelCacheClientFactory = multilevelCacheClientFactory;
    
            [HttpGet]
            public async Task<string?> TestAsync(string value= "MultilevelCacheFactoryValue")
            {
                using var multilevelCacheClient = _multilevelCacheClientFactory.Create();
                var cacheValue = await multilevelCacheClient.GetAsync<string>(key);
                if (cacheValue != null)
                {
                    Console.WriteLine($"use factory get data by multilevel cache:【{cacheValue}】");
                    return cacheValue;
                }
                cacheValue = value;
                Console.WriteLine($"use factory write data【{cacheValue}】to multilevel cache");
                await multilevelCacheClient.SetAsync(key, cacheValue);
                return cacheValue;
            }
        }
    
  2. 运行程序

    • 我这边启动以命令行启动了两个服务模拟不同服务或者集群

      dotnet run --urls=http://*:2001
      dotnet run --urls=http://*:2002
      
    • 在端口2001的程序写入数据之后,端口2002的程序能够读取到数据

    • 在端口2001的程序修改 缓存数据 ,端口2002的程序能够同步新的缓存数据过来

总结

其实任何语言都能实现这个多级缓存功能,我们去看框架源码,不仅是对功能原理的探索,也是学习别人的设计思想。

  • 提升访问速度,降低分布式缓存压力:masa的多级缓存优先从内存读取数据,提高程序访问速度。间接减少网络请求,降低了分布式缓存的压力。

  • 缓存高度扩展性:MASA Framework的缓存组件可以支持自己去实现自己的缓存逻辑,比如说目前masa的分布式缓存使用redis,我想用其它的缓存组件,或者我觉得masa实现的不优雅,完全可以自己定制。