ABP - 缓存模块(1)
1. 与 .NET Core 缓存的关系和差异
ABP 框架中的缓存系统核心包是 Volo.Abp.Caching ,而对于分布式缓存的支持,abp 官方提供了基于 Redis 的方案,需要安装 Volo.Abp.Caching.StackExchangeRedis 集成包。默认的情况下,在我们使用 ABP CLI 创建 ABP 框架模板项目的时候已经集成了这个包,我们不需要手动进行安装。
ABP 框架中的缓存系统在 ASP.NET Core的分布式缓存系统上进行扩展,从而为了使分布式缓存使用起来更加方便。理所当然的,ABP 框架的缓存系统兼容 ASP.NET Core 原生的分布式缓存使用方式,关于 ASP.NET Core 分布式缓存的使用,可以参考我之前的文章 ASP.NET Core - 缓存之分布式缓存,也可以看下官方文档 ASP.NET Core 中的分布式缓存 。
那么 ABP 框架中的缓存系统扩展了什么呢?ASP.NET Core 的分布式缓存又有哪些不便之处呢 ?ASP.NET Core 通过 IDistributedCache 抽象了分布式缓存的存取操作,但是有这个两个问题:
它对缓存值的存取是基于 byte 数组的,而不是对象。
这使得我们在使用分布式缓存的时候需要先将实例对象进行序列化/反序列化,之后再转码为 byte 数组。如果每个使用分布式缓存的地方都这么做将会有很多的重复冗余的代码,这是需要抽取封装的。
为了能实现多个应用公用缓存,达成分布式缓存的左右,它将所有的缓存项存放在同一个 Key 池之中。
这就可能会有问题,我们要特别注意缓存键的设置,如果开发人员不注意,很可能将一些缓存相互覆盖了,特别是共用缓存的应用比较多,或者多租户的情况下,这造成的问题可能很严重,而且很难排查。
ABP 框架的缓存系统扩展了通用的泛型接口 IDistributedCache<TCacheItem>
,泛型类型就是缓存值的类型,用于解决上面提到的问题:
该接口内部实现了对缓存对象 序列化/反序列化 的逻辑。 默认使用 JSON 序列化, 我们如果有需要可以替换 依赖注入 系统中 IDistributedCacheSerializer 服务的实现来覆盖默认的方式。
该接口会根据缓存中对象类型自动向缓存key添加 缓存名称 前缀。 默认缓存名是缓存对象的全类名(如果类名以CacheItem 结尾, 那么CacheItem 会被忽略,不应用到缓存名称上),开发人员也可以在缓存类上使用 CacheName 特性 设置缓存的名称.
如果是多租户应用的话,它会自动将当前的租户id添加到缓存键中, 以区分不同租户的缓存项 。 如果租户之间需要共享缓存对象, 我们可以在缓存类打上 IgnoreMultiTenancy 特性,声明当前缓存不区分组合。
允许为每个应用程序定义 全局缓存键前缀 , 不同的应用程序可以在共享的分布式缓存中拥有自己的隔离池.
它提供了 错误容忍 机制,对分布式缓存存取过程中的异常进行了处理,例如分布式缓存服务连接失败等,避免因缓存问题导致应用出错。
因为缓存本身就只是一种用于提升应用性能的策略,如果因为分布式缓存的问题导致应用出错,业务逻辑无法继续执行,那就得不偿失了。缓存系统应该是,就算将其从应用中摘除,也不影响应用业务逻辑正常执行的东西,只是性能可能差了点。它额外提供了 GetManyAsync 和 SetManyAsync 等方法, 支持对缓存的批量操作,可以显著提高批处理的性能。
2. Abp 缓存的使用
这里还是以控制台应用作为演示,Web 应用中使用比控制台更简单,因为 Web 应用中已经集成了缓存模块,不需要再自行引入。首先,通过以下命令生成一个 ABP 的控制台启动模板:
abp new AbpCacheSample -t console
之后,如果是使用基于内存的分布式缓存的话,只需要安装 Volo.Abp.Caching 包即可。
Install-package Volo.Abp.Caching
之后,在项目的模块文件中添加模块依赖:
这里不需要再进行依赖关系的配置,因为在 AbpCacheModule模块的初始化之中已经配置好了相关的内容,通过源码可以看到:
当然,你也可以更高效地在项目所在的文件夹使用以下命令,省掉对模块依赖的配置。
abp add-package Volo.Abp.Caching
如果是使用 Redis 分布式缓存的话,需要安装 Volo.Abp.Caching.StackExchangeRedis 集成包,这个包对 Microsoft.Extensions.Caching.StackExchangeRedis 进行了扩展,它简化了 Redis 缓存的配置,也是前面提到的 GetManyAsync 和SetManyAsync 等更加性能的方法的实现所在。
install-package Volo.Abp.Caching.StackExchangeRedis
同时,模块依赖改成以下这样:
同样的,模块初始化的时候也已经配置好 Redis 缓存使用的内容:
通过源码我们也可以看到,Redis 相应的配置是从配置文件中的 "Redis" 节点读取的,我们可以在appsettings.json 中添加以下内容启用 Redis 缓存:
{
"Redis": {
"IsEnabed": true, // 控制 Redis 分布式缓存是否启用
"Configuration": "xxx.xxx.xxx.xxx:6379,password=123456" // Redis 连接字符串
}
}
这里其实就是通过配置信息中的 "Redis:Configuration" 节点配置 RedisCacheOpions 选项,这个选项是微软标准的 Redi s缓存支持包中的,如果有需要,我们可以在代码中通过以下方式对该选项进行配置:
[DependsOn(
typeof(AbpAutofacModule),
typeof(AbpCachingStackExchangeRedisModule)
)]
public class AbpCacheSampleModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<RedisCacheOptions>(option =>
{
// ...
});
}
}
2.1 常规使用
首先我们定义缓存类:
/// <summary>
/// 缓存中,会以缓存类的全类名作为建,如果类名以 CacheItem 结尾,CacheItem 会被忽略
/// </summary>
// [CacheName("DateTime")] 也可以通过 CacheName 特性设置缓存键
public class DateTimeCacheItem
{
public DateTime Now { get; set; }
public string Name { get; set; }
}
之后,只需要在要用到的服务中注入 IDistributedCache 泛型接口即可:
public class HelloWorldService : ITransientDependency
{
public const string CacheKey = nameof(HelloWorldService);
private readonly IDistributedCache<DateTimeCacheItem> _distributedCache;
public HelloWorldService(IDistributedCache<DateTimeCacheItem> distributedCache)
{
_distributedCache = distributedCache;
}
public async Task SayHelloAsync()
{
// 常规的 IDisctributedCache 的同名方法,不过 ABP 框架中进行了扩展
// 使其支持泛型,可以直接存取对象
//var cacheValue = await _distributedCache.GetAsync(CacheKey);
//if (cacheValue == null)
//{
// cacheValue = new DateTimeCacheItem
// {
// Name = CacheKey,
// Now = DateTime.Now,
// };
// await _distributedCache.SetAsync(
// CacheKey, // 缓存键,最终的键会是 缓存名称(缓存类全类名去除CacheItem,或CacheName特性设置的名称) + 这里的键
// cacheValue, // 直接存取对象,而不用自己序列化/反序列化 以及转码
// new DistributedCacheEntryOptions // 一样可以通过选项设置缓存策略
// {
// SlidingExpiration = TimeSpan.FromMinutes(1)
// });
//}
// ABP 框架新增的方法
var cacheValue = await _distributedCache.GetOrAddAsync(
CacheKey,
async () =>
{
return await Task.FromResult(new DateTimeCacheItem()
{
Name = CacheKey,
Now = DateTime.Now
});
},
() => new DistributedCacheEntryOptions
{
SlidingExpiration= TimeSpan.FromMinutes(1)
});
Console.WriteLine(JsonSerializer.Serialize(cacheValue));
Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
}
}
2.2 非字符串类型的 Key
除了可以使用 string 类型作为缓存键之外,我们还可以通过 IDistributedCache<CacheKey, CacheItem> 接口使用其他类型,甚至复杂类型作为缓存键,使用复杂类型作为缓存键的时候需要重写 ToString() 方法。
public class MyCacheKey
{
public int Id { get; set; }
public string Name { get; set; }
public override string ToString()
{
return Id + Name;
}
}
public class HelloWorldService : ITransientDependency
{
private readonly IDistributedCache<DateTimeCacheItem, int> _distributedCacheKeyInt;
private readonly IDistributedCache<DateTimeCacheItem, MyCacheKey> _distiributedCacheKey;
public HelloWorldService(
IDistributedCache<DateTimeCacheItem, int> distributedCacheKeyInt,
IDistributedCache<DateTimeCacheItem, MyCacheKey> distiributedCacheKey)
{
_distributedCache = distributedCache;
}
public async Task SayHelloAsync()
{
_ = await _distributedCacheKeyInt.GetOrAddAsync(
1,
async () =>
{
return await Task.FromResult(new DateTimeCacheItem
{
Name = "1",
Now = DateTime.Now
});
},
() => new DistributedCacheEntryOptions { SlidingExpiration = TimeSpan.FromSeconds(10) }
);
_ = await _distiributedCacheKey.GetOrAddAsync(
new MyCacheKey { Id = 1, Name = "MyKey" },
async () =>
{
return await Task.FromResult(new DateTimeCacheItem
{
Name = "1",
Now = DateTime.Now
});
},
() => new DistributedCacheEntryOptions { SlidingExpiration = TimeSpan.FromSeconds(10) }
);
}
}
2.3 批量操作
批量进行缓存存取操作的方式如下:
_ = await _distributedCache.GetOrAddManyAsync(new List<string> { "Key1", "Key2" }, async keys =>
{
return await Task.FromResult(
keys.Select(k => new KeyValuePair<string, DateTimeCacheItem>(k, new DateTimeCacheItem { Name = k, Now = DateTime.Now }))
.ToList()
);
},
() => new DistributedCacheEntryOptions { SlidingExpiration = TimeSpan.FromSeconds(10) });
ABP的分布式缓存接口定义了以下批量操作方法,当你需要在一个方法中调用多次缓存操作时,这些方法可以提高性能
- SetManyAsync 和 SetMany 方法可以用来向缓存中设置多个值.
- GetManyAsync 和 GetMany 方法可以用来从缓存中获取多个值.
- GetOrAddManyAsync 和 GetOrAddMany 方法可以用来从缓存中获取并添加缺少的值.
- RefreshManyAsync 和 RefreshMany 方法可以来用重置多个值的滚动过期时间.
- RemoveManyAsync 和 RemoveMany 方法可以用来从缓存中删除多个值.
这些不是标准的ASP.NET Core缓存方法, 所以其他的分布式缓存方案可能不支持。而 ABP 通过 Volo.Abp.Caching.StackExchangeRedis 实现了 Redis 分布式缓存下的批量操作。如果采用了其他方案实现分布式缓存,而提供程序不支持的情况下,会回退到循环调用 SetAsync 和 GetAsync 方法。
以上就是 ABP 框架中缓存系统的基本使用。除此之外,ABP 框架提供了 AbpDistributedCacheOptions 选项用于配置一些缓存策略,可用属性:
- HideErrors (bool, 默认: true): 启用/禁用隐藏从缓存服务器写入/读取值时的错误.
- KeyPrefix (string, 默认: null): 如果你的缓存服务器由多个应用程序共同使用, 则可以为应用程序的缓存键设置一个前缀. 在这种情况下, 不同的应用程序不能覆盖彼此的缓存内容.
- GlobalCacheEntryOptions (DistributedCacheEntryOptions): 用于设置保存缓内容却没有指定选项时, 默认的分布式缓存选项 (例如 AbsoluteExpiration 和 SlidingExpiration). SlidingExpiration的默认值设置为20分钟.
3. 额外功能
另外,还有ABP 框架下的缓存系统还有以下一些功能:
错误处理
当为你的对象设计缓存时, 通常会首先尝试从缓存中获取值. 如果在缓存中找不到该值, 则从来源查询对象. 它可能在数据库中, 或者可能需要通过HTTP调用远程服务器.
在大多数情况下, 你希望容忍缓存错误; 如果缓存服务器出现错误, 也不希望取消该操作. 相反, 你可以默默地隐藏(并记录)错误并从来源查询. 这就是ABP框架默认的功能.
ABP的分布式缓存异常处理, 默认记录并隐藏错误,有一个全局修改该功能的选项.所有的IDistributedCache (和 IDistributedCache<TCacheItem, TCacheKey>)方法都有一个可选的参数hideErrors, 默认值为null. 如果此参数设置为null, 则全局生效, 否则你可以选择单个方法调用时隐藏或者抛出异常.
工作单元级别的缓存
分布式缓存服务提供了一个有趣的功能. 假设你已经更新了数据库中某本书的价格, 然后将新价格设置到缓存中, 以便以后使用缓存的值. 如果设置缓存后出现异常, 并且更新图书价格的事务被回滚了, 该怎么办?在这种情况下, 缓存值是错误的.
IDistributedCache<..>方法提供一个可选参数, considerUow, 默认为false. 如果将其设置为true, 则你对缓存所做的更改不会应用于真正的缓存存储, 而是与当前的工作单元关联. 你将获得在同一工作单元中设置的缓存值, 但仅当前工作单元成功时更改才会生效.
可替换的键、值处理方式
IDistributedCacheSerializer
IDistributedCacheSerializer 服务用于序列化和反序列化缓存内容. 默认实现是 Utf8JsonDistributedCacheSerializer 类, 它使用 IJsonSerializer 服务将对象转换为 JSON, 反之亦然. 然后, 它使用 UTC8 编码将 JSON 字符串转换为分布式缓存接受的字节数组.
如果你想实现自己的序列化逻辑, 可以自己实现并替换此服务.
IDistributedCacheKeyNormalizer
默认情况下, IDistributedCacheKeyNormalizer是由DistributedCacheKeyNormalizer类实现的. 它将缓存名称、应用程序缓存前缀和当前租户id添加到缓存键中. 如果需要更高级的键规范化, 可以自己实现并替换此服务.
参考文章:
ABP 官方文档 - 缓存
ABP 系列总结:
目录:ABP 系列总结
上一篇:ABP - 依赖注入(2)
ABP - 缓存模块(1)的更多相关文章
- 基于DDD的.NET开发框架 - ABP缓存Caching实现
返回ABP系列 ABP是“ASP.NET Boilerplate Project (ASP.NET样板项目)”的简称. ASP.NET Boilerplate是一个用最佳实践和流行技术开发现代WEB应 ...
- 【Java EE 学习 78 上】【数据采集系统第十天】【Service使用Spring缓存模块】
一.需求分析 调查问卷中或许每一个单击动作都会引发大量的数据库访问,特别是在参与调查的过程中,只是单击“上一页”或者“下一页”的按钮就会引发大量的查询,必须对这种问题进行优化才行.使用缓存策略进行查询 ...
- .NET 缓存模块设计
上一篇谈了我对缓存的概念,框架上的理解和看法,这篇承接上篇讲讲我自己的缓存模块设计实践. 基本的缓存模块设计 最基础的缓存模块一定有一个统一的CacheHelper,如下: public interf ...
- ABP之模块
ABP的反射 为什么先讲反射,因为ABP的模块管理基本就是对所有程序集进行遍历,再筛选出AbpModule的派生类,再按照以来关系顺序加载. ABP对反射的封装着重于程序集(Assembly)与类(T ...
- IOS编程 图片缓存模块设计
手机客户端为什么会留存下来?而不是被一味的Wap替代掉?因为手机客户端有Wap无可替代的优势,就是自身较强的计算能力. 手机中不可避免的一环:图片缓存,在软件的整个运行过程中显得尤为重要. 先简单说一 ...
- 使用spring EL表达式+自定义切面封装缓存模块
需求是这样的,业务代码需要使用到缓存功能以减少数据库压力,使用redis来实现,并且需要生成缓存的key由方法的传参拼接而成(貌似也只能这样才能保证同样的select查询可以使用缓存),简单的方式就是 ...
- 一步步实现一个基本的缓存模块·续, 添加Memcached调用实现
jusfr 原创,转载请注明来自博客园. 在之前的实现中,我们初步实现了一个缓存模块:包含一个基于Http请求的缓存实现,一个基于HttpRuntime.Cache进程级的缓存实现,但观察代码,会发现 ...
- 简单的内存缓存模块 - Smache
介绍 [sm]art + c[ache] = smache Smache 是一个方便的内存缓存模块,可以通过一些简单缓存策略避免无限占用更多的内存,同时确保最常用最应该被缓存的对象被缓存. GitHu ...
- 【构建Android缓存模块】(一)吐槽与原理分析
http://my.oschina.net/ryanhoo/blog/93285 摘要:在我翻译的Google官方系列教程中,Bitmap系列由浅入深地介绍了如何正确的解码Bitmap,异步线程操作以 ...
- jQuery 源码分析(十) 数据缓存模块 data详解
jQuery的数据缓存模块以一种安全的方式为DOM元素附加任意类型的数据,避免了在JavaScript对象和DOM元素之间出现循环引用,以及由此而导致的内存泄漏. 数据缓存模块为DOM元素和JavaS ...
随机推荐
- Go语言:利用 TDD 测试驱动开发帮助理解数组与动态数组(切片)的区别
Array VS Slice 数组允许你以特定的顺序在变量中存储相同类型的多个元素. 对于数组来说,最常见的就是迭代数组中的元素. 我们创建一个 Sum 函数,它使用 for 来循环获取数组中的元素并 ...
- 64位的单周期 RISC-V 模拟器
分享一个我最近完成过的小项目--64位的单周期 RISC-V 模拟器,这个项目我最近参与一生一芯计划过程中完成的一个小项目. 需要用到的相关知识:Verilog.Verilator.计算机组成原理.汇 ...
- 记一次 .NET 某医疗住院系统 崩溃分析
一:背景 1. 讲故事 最近收到了两起程序崩溃的dump,查了下都是经典的 double free 造成的,蛮有意思,这里就抽一篇出来分享一下经验供后面的学习者避坑吧. 二:WinDbg 分析 1. ...
- 算法题学习链路简要分析与面向 ChatGPT 编程
本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问. 声明:此图片由 MidJourney 生成 未经训练,不属于任何真实人物 大家好,我是小彭. 2023 开年 ...
- 线性规划与整数规划—R实现
线性规划的R语言实现 R语言在针对各类优化模型时都能快速方便的求解,对运输问题.生产计划问题.产销问题和旅行商问题等都有专门的R包来解决.线性规划与整数规划的区别主要在于对决策变量的取值约束有所不同. ...
- 从ReentrantLock角度解析AQS
是它,是它,就是它,并发包的基石: 一.概述 闲来不卷,随便聊一点. 一般情况下,大家系统中至少也是JDK8了,那想必对于JDK5加入的一系列功能并不陌生吧.那时候重点加入了java.util.con ...
- Redis(四)主从复制
主从复制 简介 主机数据更新之后根据配置和策略,自动同步数据到备机的Master/Slaver机制,Master以写为主,Slaver以读为主. 这样的机制能够实现: 读写分离:Master以写为主, ...
- 使用 Transformers 进行图分类
在之前的 博文 中,我们探讨了图机器学习的一些理论知识.这一篇我们将探索如何使用 Transformers 库进行图分类.(你也可以从 此处 下载演示 notebook,跟着一起做!) 目前,Tran ...
- 【Python毕业设计】基于Python+Flask+MySQL的学生信息管理系统(附完整源码)
1.项目说明基于python+Flask+mysql的学生信息管理系统项目实战 项目需要安装pycharm专业版,mysql数据库以及项目所需的所有模块创建数据库名称db_online_notes,然 ...
- 今天能恢复我的Django吗——恢复了!
今天能用两小时恢复我的Django吗 实在是累了,昨天和队友改bug的时候为了能在我的电脑上实现他的程序就在datagrip中删了我django建的表.没想到啊,这一删就全是报错!! 不说了,今天看看 ...