Masa Framework源码解读-02缓存模块(分布式缓存进阶之多级缓存)
序言
今天这篇文章来看看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.Caching、Masa.Contrib.Caching.Distributed.StackExchangeRedis、Masa.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连接、订阅、配置等初始化。继承DistributedCacheClientBaseRedisCacheClient:分布式缓存的redis实现,继承RedisCacheClientBase
Masa.Contrib.Caching.MultilevelCache:多级缓存实现- MultilevelCacheClient :多级缓存实现,内部依赖
IDistributedCacheClient。
- MultilevelCacheClient :多级缓存实现,内部依赖
整个缓存组件的设计,最主要类是这些,当然还有一些option配置和帮助类,我就没有画出来,这个留待大家自己去探索
Demo案例
Demo案例项目地址:https://github.com/MapleWithoutWords/masa-demos/tree/main/src/CachingDemo
上面也说到Masa Framework的缓存组件不与框架强绑定,也就是说我们可以在自己的框架中使用masa的缓存组件,下面我将展示两个项目,它们分别使用分布式缓存和多级缓存。
分布式缓存使用
- 第一步,在我们的项目中安装分布式缓存组件
Masa.Contrib.Caching.Distributed.StackExchangeRedis(下载1.0.0-preview.18版本以上),或在项目目录下使用命令行安装
dotnet add package Masa.Contrib.Caching.Distributed.StackExchangeRedis --version 1.0.0-preview.18
- 第二步,在Program.cs文件中添加以下代码
builder.Services.AddDistributedCache(opt =>
{
opt.UseStackExchangeRedisCache();
});
- 第三步,在配置文件中增加以下配置。这边再补充以下,masa的redis分布式缓存是支持集群的,只需要在Servers下配置多个节点就行
"RedisOptions": {
"Servers": [
{
"Host": "127.0.0.1",
"Port": "6391"
}
],
"DefaultDatabase": 0,
"Password": "123456"
}
第四步:在构造函数中注入
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;
}
}
}
- 构造函数中注入
最终结果:注:记得启动本地redis

多级缓存使用
- 第一步,在我们的项目中安装组件:
Masa.Contrib.Caching.MultilevelCache和Masa.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
- 第二步,在Program.cs文件中添加以下代码
builder.Services.AddMultilevelCache(opt =>
{
opt.UseStackExchangeRedisCache();
});
- 第三步,在配置文件中增加以下配置。多级缓存依赖于分布式缓存,所以需要添加redis配置,如下:
"RedisOptions": {
"Servers": [
{
"Host": "127.0.0.1",
"Port": "6391"
}
],
"DefaultDatabase": 0,
"Password": "123456"
}
第四步:在构造函数中注入
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);
}
}
- 构造函数中注入
运行程序
我这边启动以命令行启动了两个服务模拟不同服务或者集群
dotnet run --urls=http://*:2001
dotnet run --urls=http://*:2002
在端口2001的程序写入数据之后,端口2002的程序能够读取到数据

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

总结
其实任何语言都能实现这个多级缓存功能,我们去看框架源码,不仅是对功能原理的探索,也是学习别人的设计思想。
提升访问速度,降低分布式缓存压力:masa的多级缓存优先从内存读取数据,提高程序访问速度。间接减少网络请求,降低了分布式缓存的压力。
缓存高度扩展性:MASA Framework的缓存组件可以支持自己去实现自己的缓存逻辑,比如说目前masa的分布式缓存使用redis,我想用其它的缓存组件,或者我觉得masa实现的不优雅,完全可以自己定制。
Masa Framework源码解读-02缓存模块(分布式缓存进阶之多级缓存)的更多相关文章
- 虎说:bootstrap源码解读(重置模块)
------<!--action-->------ 开场show:前不生“不犹豫”,后半生“不后悔”.今天又逃课,我不后悔 素材:推特公司的前端框架bootstrap(下称bt),解读源码 ...
- Vue源码学习02 初始化模块init.js
接上篇,我们看到了VUE分了很多模块(initMixin()stateMixin()eventsMixin()lifecycleMixin()renderMixin()),通过使用Mixin模式,都是 ...
- jvm源码解读--02 Array<u1>* tags = MetadataFactory::new_writeable_array<u1>(loader_data, length, 0, CHECK_NULL); 函数引入的jvm内存分配解析
current路径: #0 Array<unsigned char>::operator new (size=8, loader_data=0x7fd4c802e868, length=8 ...
- Webpack探索【16】--- 懒加载构建原理详解(模块如何被组建&如何加载)&源码解读
本文主要说明Webpack懒加载构建和加载的原理,对构建后的源码进行分析. 一 说明 本文以一个简单的示例,通过对构建好的bundle.js源码进行分析,说明Webpack懒加载构建原理. 本文使用的 ...
- Webpack探索【15】--- 基础构建原理详解(模块如何被组建&如何加载)&源码解读
本文主要说明Webpack模块构建和加载的原理,对构建后的源码进行分析. 一 说明 本文以一个简单的示例,通过对构建好的bundle.js源码进行分析,说明Webpack的基础构建原理. 本文使用的W ...
- Abp 审计模块源码解读
Abp 审计模块源码解读 Abp 框架为我们自带了审计日志功能,审计日志可以方便地查看每次请求接口所耗的时间,能够帮助我们快速定位到某些性能有问题的接口.除此之外,审计日志信息还包含有每次调用接口时客 ...
- AFNetworking 3.0 源码解读(十)之 UIActivityIndicatorView/UIRefreshControl/UIImageView + AFNetworking
我们应该看到过很多类似这样的例子:某个控件拥有加载网络图片的能力.但这究竟是怎么做到的呢?看完这篇文章就明白了. 前言 这篇我们会介绍 AFNetworking 中的3个UIKit中的分类.UIAct ...
- seajs 源码解读
之前面试时老问一个问题seajs 是怎么加载js 文件的 在网上找一些资料,觉得这个写的不错就转载了,记录一下,也学习一下 seajs 源码解读 seajs 简单介绍 seajs是前端应用模块化开发的 ...
- SDWebImage源码解读之SDWebImagePrefetcher
> 第十篇 ## 前言 我们先看看`SDWebImage`主文件的组成模块: 
解决办法 var u = navigator.userAgent; var isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); //ios终端 // ...
- JS篇(006)-怎样添加、移除、移动、复制、创建和查找节点?
答案: 1)创建新节点 createDocumentFragment() //创建一个 DOM 片段 createElement() //创建一个具体的元素 createTextNode() //创建 ...
- 查询某数据库的某字段存在于哪些表 mysql
select column_name,column_comment,data_type ,table_name from information_schema.columns where table ...
- linux系统分类
1.RedHat系列:Redhat.Centos.Fedora等 2.Debian系列:Debian.Ubuntu等 RedHat 系列 1 常见的安装包格式 rpm包,安装rpm包的命令是" ...
- QTreewidget树状列表右击事件
树状列表右击事件(添加 删除 修改等操作) 思路:首先我们需要一个void contextMenuEvent(QContextMenuEvent * event); 管理Menu事件的一个接口 此接口 ...
- vue npm安装指令汇总
1.elmentui:npm i element-ui -S 2.打印插件:npm install vue-print-nb --save 3.时间转换插件Moment:npm install mom ...
- Mysql explain 每个属性含义
Mysql explain explain 常用于分析sql语句的执行效率,使用时在正常的select语句之前添加explain并执行就会返回执行信息,返回的执行信息如下: id:id列的编号是se ...
- == 和 equal 的区别
== 比较的是两个对象的索引是否相同: equal 比较的是两个对象内容是否相同: int a = 1;long b = 1L;a==b? 答案是 对:因为a和b指向的索引地址相同. 再例如 Stri ...
- qtconsole和jupyter-notebook的对应启动命令行配置
jupyter-notebook: set conda_path=D:\use\program\Miniconda3 pushd %conda_path% call Scripts\activate. ...
- Vue+SSM+Element-Ui实现前后端分离(3)
前言:经过博文(1)vue搭建,(2)ssm搭建,到这里就该真真切切的实现小功能了<-_-> 规划:实现登录,用户列表查询,访问日志aop; 开始先解决跨域:在目录config/index ...