ABP演示 - 缓存
1. 缓存功能概述
在 ABP 框架中实现缓存功能主要涉及以下核心组件:
- 分布式缓存接口
IDistributedCache<T> - 缓存项定义 (如
BookCacheItem) - 缓存键生成器 (如
BookCacheKeyGenerator) - 批量缓存操作 (如
BookCacheBulkOperations) - 缓存失效处理器 (如
BookCacheInvalidator)
2. 实现步骤
基础版:IDistributedCache<TCacheItem>
- 缓存键只能是
string类型,所有场景都需要手动拼接(如Product:1、Order:2026:1001); - 无类型约束,容易因键格式错误导致缓存命中失败(如把
Guid直接转字符串、多参数拼接顺序错误)。
2.1 定义缓存项
不加 [CacheName]:{缓存项类型的全限定名}:{缓存键}(如 Acme.BookStore.CacheItems.BookCacheItem:1);
加 [CacheName("Books")]:{CacheName值}:{缓存键}(如 Books:1)
| 特性 | 不加 [CacheName] |
加 [CacheName] |
|---|---|---|
| 键长度 | 较长(含命名空间) | 短(仅指定名称) |
| 可读性 | 较低 | 更高 |
| 重构影响 | 类重命名会导致缓存失效 | 类重命名不影响缓存键 |
| 多模块冲突风险 | 不同模块的同名类会冲突 | 可全局统一命名避免冲突 |
| Redis可视化 | 键名冗长 | 更清晰的层级结构(如 Books:*) |
public class BookCacheItem
{
public Guid Id { get; set; }
public string Name { get; set; }
public BookType Type { get; set; }
public DateTime PublishDate { get; set; }
public float Price { get; set; }
}
2.2 创建缓存键生成器
public class BookCacheKeyGenerator : ITransientDependency
{
public const string CachePrefix = "Book:";
public string GetCacheKey(string id) => $"{CachePrefix}{id}";
public string GetCacheKey(Guid id) => $"{CachePrefix}{id}";
}
2.3 实现批量缓存操作
ABP的分布式缓存接口定义了以下批量操作方法,当你需要在一个方法中调用多次缓存操作时,这些方法可以提高性能
SetManyAsync和SetMany方法可以用来向缓存中设置多个值.GetManyAsync和GetMany方法可以用来从缓存中获取多个值.GetOrAddManyAsync和GetOrAddMany方法可以用来从缓存中获取并添加缺少的值.RefreshManyAsync和RefreshMany方法可以来用重置多个值的滚动过期时间.RemoveManyAsync和RemoveMany方法可以用来从缓存中删除多个值.
这些不是标准的ASP.NET Core缓存方法, 所以某些提供程序可能不支持. [ABP Redis集成包]实现了它们. 如果提供程序不支持,会回退到
SetAsync和GetAsync... 方法(循环调用).
namespace Acme.BookStore.Caching
{
public class BookCacheBulkOperations : ITransientDependency
{
private readonly IDistributedCache<BookCacheItem> _cache;
private readonly BookCacheKeyGenerator _cacheKeyGenerator;
public BookCacheBulkOperations(
IDistributedCache<BookCacheItem> cache,
BookCacheKeyGenerator cacheKeyGenerator)
{
_cache = cache;
_cacheKeyGenerator = cacheKeyGenerator;
}
public async Task SetManyAsync(Dictionary<Guid, BookCacheItem> items)
{
var cacheEntries = items.ToDictionary(
x => _cacheKeyGenerator.GetCacheKey(x.Key),
x => x.Value);
await _cache.SetManyAsync(cacheEntries, new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
});
}
public async Task<Dictionary<Guid, BookCacheItem>> GetManyAsync(IEnumerable<Guid> ids)
{
var cacheKeys = ids.Select(_cacheKeyGenerator.GetCacheKey).ToList();
var cachedItems = await _cache.GetManyAsync(cacheKeys);
return cachedItems.ToDictionary(
x => Guid.Parse(x.Key.Replace(BookCacheKeyGenerator.CachePrefix, "")),
x => x.Value);
}
public async Task RemoveManyAsync(IEnumerable<Guid> ids)
{
var cacheKeys = ids.Select(_cacheKeyGenerator.GetCacheKey).ToList();
await _cache.RemoveManyAsync(cacheKeys);
}
}
}
2.4 在应用服务中使用缓存
public override async Task<BookDto> GetAsync(Guid id)
{
var cacheKey = _cacheKeyGenerator.GetCacheKey(id);
// 尝试从缓存获取
var cachedItem = await _cache.GetAsync(cacheKey);
if (cachedItem != null)
{
return ObjectMapper.Map<BookCacheItem, BookDto>(cachedItem);
}
// 缓存未命中,从数据库获取
var book = await base.GetAsync(id);
// 存入缓存
var cacheItem = ObjectMapper.Map<BookDto, BookCacheItem>(book);
await _cache.SetAsync(
cacheKey,
cacheItem,
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
});
return book;
}
2.5 实现缓存失效(监听,按需求)
缓存失效是什么?
想象一个场景:
-
你第一次查图书"三国演义",系统从数据库读取并存入缓存
-
你第二次查"三国演义",系统直接从缓存读取,很快
-
现在有人修改了书名改成"三国演义之赤壁之战"
-
问题:缓存里还是"三国演义",不是最新的!
缓存失效就是解决这个问题:当数据更新时,自动删除旧缓存,下次查询时重新加载。
它是自动工作的吗?
是的!只要:
BookCacheInvalidator类存在(已创建)- 继承了
ILocalEventHandler<EntityUpdatedEventData<Book>>- 注册为
ITransientDependency(ABP会自动扫描并注册)
EntityCreatedEventData<TEntity>:实体创建后触发;
EntityDeletedEventData<TEntity>:实体删除后触发;三者共同继承
EntityChangedEventData<TEntity>,处理逻辑可复用。参考:
ABP - 实体变更监视事件 [EntityCreatedEventData、EntityUpdatedEventData、EntityDeletedEventData]
[ABP - 事件总线(Event Bus)IEventBus、LocalEventBus、IntegrationEvent]
满足以上条件后,每当 Book 实体更新时,ABP 会自动调用这个处理器清理缓存。
namespace Acme.BookStore.Caching
{
public class BookCacheInvalidator :
ILocalEventHandler<EntityUpdatedEventData<Book>>,
ITransientDependency
{
private readonly IDistributedCache<BookCacheItem> _cache;
private readonly BookCacheKeyGenerator _cacheKeyGenerator;
public BookCacheInvalidator(
IDistributedCache<BookCacheItem> cache,
BookCacheKeyGenerator cacheKeyGenerator)
{
_cache = cache;
_cacheKeyGenerator = cacheKeyGenerator;
}
public async Task HandleEventAsync(EntityUpdatedEventData<Book> eventData)
{
var cacheKey = _cacheKeyGenerator.GetCacheKey(eventData.Entity.Id);
await _cache.RemoveAsync(cacheKey);
}
}
}
2.7 为应用程序设置缓存键前缀 AbpDistributedCacheOptions
AbpDistributedCacheOptions 是配置缓存的主要[Option类].
示例:为应用程序设置缓存键前缀
Configure<AbpDistributedCacheOptions>(options =>
{
options.KeyPrefix = "MyApp1";
});
在[模块类]的
ConfigureServices方法中添加代码.
可用选项
HideErrors(bool, 默认:true): 启用/禁用隐藏从缓存服务器写入/读取值时的错误.KeyPrefix(string, 默认:null): 如果你的缓存服务器由多个应用程序共同使用, 则可以为应用程序的缓存键设置一个前缀. 在这种情况下, 不同的应用程序不能覆盖彼此的缓存内容.GlobalCacheEntryOptions(DistributedCacheEntryOptions): 用于设置保存缓内容却没有指定选项时, 默认的分布式缓存选项 (例如AbsoluteExpiration和SlidingExpiration).SlidingExpiration的默认值设置为20分钟.
增强版:IDistributedCache<TCacheItem, TCacheKey>
- 缓存键支持任意自定义类型(如
Guid、int、自定义ProductCacheKey类); - 框架内置键的序列化 / 反序列化逻辑,无需手动拼接字符串键。
IDistributedCache<TCacheItem> 接口默认了缓存键是 string 类型 (如果你的键不是string类型需要进行手动类型转换). 但当缓存键的类型不是string时, 可以使用IDistributedCache<TCacheItem, TCacheKey>.
示例: 在缓存中存储图书名称和价格
示例缓存项
using Volo.Abp.Caching;
namespace MyProject
{
[CacheName("Books")]
public class BookCacheItem
{
public string Name { get; set; }
public float Price { get; set; }
}
}
- 在本例中使用
CacheName特性给BookCacheItem类设置缓存名称.
你可以注入 IDistributedCache<BookCacheItem, Guid> 服务用于 get/set BookCacheItem 对象.
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
namespace MyProject
{
public class BookService : ITransientDependency
{
private readonly IDistributedCache<BookCacheItem, Guid> _cache;
public BookService(IDistributedCache<BookCacheItem, Guid> cache)
{
_cache = cache;
}
public async Task<BookCacheItem> GetAsync(Guid bookId)
{
return await _cache.GetOrAddAsync(
bookId, //Guid类型作为缓存键
async () => await GetBookFromDatabaseAsync(bookId),
() => new DistributedCacheEntryOptions
{
AbsoluteExpiration = DateTimeOffset.Now.AddHours(1)
}
);
}
private Task<BookCacheItem> GetBookFromDatabaseAsync(Guid bookId)
{
//TODO: 从数据库获取数据
}
}
}
- 示例服务中
GetOrAddAsync()方法获取缓存的图书项. - 我们采用了
Guid做为键, 在_cache_GetOrAddAsync()方法中传入Guid类型的bookid.
2.8 复杂类型的缓存键
IDistributedCache<TCacheItem, TCacheKey> 在内部使用键对象的 ToString() 方法转换类型为string. 如果你的将复杂对象做为缓存键,那么需要重写类的 ToString 方法.
举例一个作为缓存键的类:
public class UserInOrganizationCacheKey
{
public Guid UserId { get; set; }
public Guid OrganizationId { get; set; }
//构建缓存key
public override string ToString()
{
return $"{UserId}_{OrganizationId}";
}
}
用法示例:
public class BookService : ITransientDependency
{
private readonly IDistributedCache<UserCacheItem, UserInOrganizationCacheKey> _cache;
public BookService(
IDistributedCache<UserCacheItem, UserInOrganizationCacheKey> cache)
{
_cache = cache;
}
...
}
3. 高级主题
工作单元级别的缓存
分布式缓存服务提供了一个有趣的功能. 假设你已经更新了数据库中某本书的价格, 然后将新价格设置到缓存中, 以便以后使用缓存的值. 如果设置缓存后出现异常, 并且更新图书价格的事务被回滚了, 该怎么办?在这种情况下, 缓存值是错误的.
IDistributedCache<..>方法提供一个可选参数, considerUow, 默认为false. 如果将其设置为true, 则你对缓存所做的更改不会应用于真正的缓存存储, 而是与当前的[工作单元]关联. 你将获得在同一工作单元中设置的缓存值, 但仅当前工作单元成功时更改才会生效.
IDistributedCacheSerializer
IDistributedCacheSerializer服务用于序列化和反序列化缓存内容. 默认实现是Utf8JsonDistributedCacheSerializer类, 它使用IJsonSerializer服务将对象转换为[JSON], 反之亦然. 然后, 它使用UTC8编码将JSON字符串转换为分布式缓存接受的字节数组.
如果你想实现自己的序列化逻辑, 可以自己实现并[替换] 此服务.
IDistributedCacheKeyNormalizer
默认情况下, IDistributedCacheKeyNormalizer是由DistributedCacheKeyNormalizer类实现的. 它将缓存名称、应用程序缓存前缀和当前租户id添加到缓存键中. 如果需要更高级的键规范化, 可以自己实现并[替换] 此服务.
4. Redis 缓存
ABP Framework缓存系统扩展了ASP.NETCore分布式缓存。因此,标准ASP.NETCore分布式缓存支持的任何提供程序都可以在您的应用程序中使用,并且可以像Microsoft记录的一样进行配置。
但是,ABP为Redis Cache提供了一个集成包:Volo. Abp.Cching.StackExchange eRedis。使用这个包而不是标准的Microsoft.Exents.Cching.StackExchange eRedis包有两个原因。
- 它实现了
SetManyAsync和GetManyAsync方法。这些不是微软缓存库的标准方法,而是由ABP框架缓存系统添加的。当您需要使用单个方法调用设置/获取多个缓存项时,它们显着提高了性能。 - 它简化了Redis缓存配置(将在下面解释)。
Volo.Abp.Caching.StackExchangeRedis 已使用 Microsoft.Extensions.Caching.StackExchangeRedis 包,但对其进行了扩展和改进。
安装
如果使用Redis,则此包已安装在应用程序启动模板中。
在.csproj文件的文件夹中打开命令行并键入以下ABP CLI命令:
abp add-package Volo.Abp.Caching.StackExchangeRedis
配置
Volo.Abp.Caching.StackExchangeRedis 包自动从IConfiguration获取Redis配置。因此,例如,您可以在appsettings.json中设置配置:
"Redis": {
"IsEnabled": "true",
"Configuration": "127.0.0.1"
}
设置IsEnabled是可选的,如果未设置,将被视为true。
或者,您可以在模块的ConfigureServices方法中配置标准RedisCacheOptions选项类:
Configure<RedisCacheOptions>(options =>
{
//...
});
5. 核心功能点
- 缓存配置:通过
DistributedCacheEntryOptions配置缓存过期时间 - 自动失效:通过事件处理器自动处理缓存失效
- 批量操作:支持批量获取和设置缓存项
- 键生成策略:统一的缓存键生成规则
6. 最佳实践
- 缓存粒度:保持缓存项尽可能小,只包含必要数据
- 过期策略:根据业务需求设置合理的过期时间
- 批量操作:对于列表查询,优先使用批量操作减少IO
- 失效机制:确保数据变更时及时失效相关缓存
7. 注意事项
- 确保所有缓存操作都是线程安全的
- 处理缓存穿透和雪崩问题
- 监控缓存命中率和性能
- 考虑分布式环境下的缓存一致性

浙公网安备 33010602011771号