ABP - 缓存(Caching)[IDistributedCache、ICacheManager、ICacheKeyNormalizer、[Cache]、[CacheInvalidate]]
(一)缓存(Caching)
核心辅助类:
IDistributedCache:分布式缓存(基于Redis等)。ICacheManager:缓存管理器(支持多级缓存)。[Cache]:方法缓存特性。ICacheKeyNormalizer:缓存键标准化器,自动添加租户前缀(多租户场景)或应用前缀。[CacheInvalidate]:缓存失效特性(修改数据时自动清除对应缓存)。
缓存(Caching)核心类详解与实战示例
缓存是提升系统性能的关键技术,通过将频繁访问的数据暂时存储在内存(或Redis等中间件)中,减少对数据库的访问次数。ABP框架提供了一套灵活的缓存机制,以下结合IDistributedCache、ICacheManager、[Cache]等核心类。
一、核心概念:为什么需要缓存?(生活例子)
想象你开了一家奶茶店:
- 顾客经常问“珍珠奶茶多少钱?”(高频查询),你每次都去查价目表(数据库),效率低;
- 你把价目表贴在吧台(缓存),顾客再问时直接看吧台,不用反复查价目表,速度快多了。
程序中的缓存也是同理:对“不常变化但经常查询的数据”(如商品分类、字典表),第一次查询后存到缓存,后续直接从缓存取,减少数据库压力。
二、核心类说明
| 类/特性 | 核心作用 | 通俗理解 |
|---|---|---|
IDistributedCache |
分布式缓存接口(支持Redis、Memcached等) | 多服务器共享的“公共缓存”(如集群部署时用) |
ICacheManager |
缓存管理器(支持内存缓存、多级缓存) | 管理不同类型的缓存,方便统一操作 |
[Cache] |
方法缓存特性(自动缓存方法返回值) | 给方法加个标签,自动缓存结果,不用写代码 |
[CacheInvalidate] |
缓存失效特性(修改数据时自动删缓存) | 数据变了,自动删掉旧缓存,避免返回脏数据 |
ICacheKeyNormalizer |
缓存键标准化(自动加前缀) | 给缓存键加“标签”(如租户ID),避免键冲突 |
三、实战示例:从基础到进阶
1. [Cache]:最简单的方法缓存(一行代码搞定)
给查询方法加[Cache]特性,框架会自动缓存方法的返回值,下次调用时直接返回缓存结果,不执行方法体。
示例:缓存商品分类列表(高频查询,低频修改)
using Volo.Abp.Caching;
using Volo.Abp.Application.Services;
public class ProductCategoryAppService : ApplicationService
{
private readonly IRepository<ProductCategory, Guid> _categoryRepo;
public ProductCategoryAppService(IRepository<ProductCategory, Guid> categoryRepo)
{
_categoryRepo = categoryRepo;
}
// 加[Cache]特性:自动缓存返回结果,默认缓存10分钟
[Cache(SlidingExpiration = 10)] // SlidingExpiration:10分钟内没人访问就失效
public async Task<List<ProductCategoryDto>> GetAllCategoriesAsync()
{
// 第一次调用会执行:查数据库
var categories = await _categoryRepo.GetListAsync();
return ObjectMapper.Map<List<ProductCategory>, List<ProductCategoryDto>>(categories);
}
}
效果:
- 第一次调用
GetAllCategoriesAsync():查数据库,结果存到缓存; - 10分钟内再次调用:直接从缓存返回,不查数据库;
- 10分钟内没人调用:缓存自动失效,下次重新查数据库。
2. [CacheInvalidate]:数据更新时自动删缓存
当数据被修改(新增/更新/删除)时,需要删除旧缓存,否则会返回过时数据。[CacheInvalidate]特性可自动完成这个操作。
示例:修改分类后删除缓存
public class ProductCategoryAppService : ApplicationService
{
// 新增分类后,删除GetAllCategoriesAsync方法的缓存
[CacheInvalidate(MethodName = nameof(GetAllCategoriesAsync))]
public async Task CreateCategoryAsync(CreateCategoryInput input)
{
var category = new ProductCategory { Name = input.Name };
await _categoryRepo.InsertAsync(category);
}
// 更新分类后,删除GetAllCategoriesAsync方法的缓存
[CacheInvalidate(MethodName = nameof(GetAllCategoriesAsync))]
public async Task UpdateCategoryAsync(Guid id, UpdateCategoryInput input)
{
var category = await _categoryRepo.GetAsync(id);
category.Name = input.Name;
await _categoryRepo.UpdateAsync(category);
}
// 之前的查询方法(带[Cache])
[Cache(SlidingExpiration = 10)]
public async Task<List<ProductCategoryDto>> GetAllCategoriesAsync()
{
// ...
}
}
效果:
- 调用
CreateCategoryAsync或UpdateCategoryAsync后,框架会自动删除GetAllCategoriesAsync方法的缓存; - 下次查询时会重新从数据库获取最新数据,并更新缓存,保证数据一致性。
3. ICacheManager:手动控制缓存(灵活场景)
当需要更灵活的缓存控制(如缓存部分数据、条件性缓存)时,用ICacheManager手动操作缓存。
示例:手动缓存用户信息
using Volo.Abp.Caching;
using Volo.Abp.DependencyInjection;
public class UserCacheService : ITransientDependency
{
private readonly ICacheManager _cacheManager;
private readonly IRepository<IdentityUser, Guid> _userRepo;
// 注入缓存管理器
public UserCacheService(ICacheManager cacheManager, IRepository<IdentityUser, Guid> userRepo)
{
_cacheManager = cacheManager;
_userRepo = userRepo;
}
// 获取用户信息(优先从缓存取)
public async Task<IdentityUser> GetUserByIdAsync(Guid userId)
{
// 1. 获取一个缓存实例(可理解为“缓存容器”,名称自定义,如"UserCache")
var cache = _cacheManager.GetCache("UserCache");
// 2. 尝试从缓存获取:key是userId,不存在则执行委托查数据库并缓存
return await cache.GetOrAddAsync(
key: userId.ToString(), // 缓存键(用userId当唯一标识)
factory: async () => await _userRepo.GetAsync(userId), // 缓存不存在时执行的逻辑
options: () => new CacheOptions { AbsoluteExpiration = DateTime.Now.AddHours(1) } // 1小时后绝对失效
);
}
// 手动删除用户缓存(如用户信息更新后)
public async Task RemoveUserCacheAsync(Guid userId)
{
var cache = _cacheManager.GetCache("UserCache");
await cache.RemoveAsync(userId.ToString()); // 删除指定key的缓存
}
}
核心方法:
GetOrAddAsync:尝试从缓存获取,不存在则执行factory方法查询并缓存;RemoveAsync:删除指定键的缓存;SetAsync:手动设置缓存(GetOrAddAsync已包含此逻辑,一般不用单独调用)。
4. IDistributedCache:分布式缓存(集群部署必备)
当系统部署在多台服务器(集群)时,内存缓存(ICacheManager默认是内存缓存)无法共享,需用IDistributedCache(如Redis)实现缓存共享。
示例:用Redis缓存全局配置
using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;
using Volo.Abp.DependencyInjection;
public class GlobalConfigService : ITransientDependency
{
private readonly IDistributedCache _distributedCache;
private readonly IRepository<GlobalConfig, Guid> _configRepo;
// 注入分布式缓存(默认是Redis,需在配置文件中配置)
public GlobalConfigService(IDistributedCache distributedCache, IRepository<GlobalConfig, Guid> configRepo)
{
_distributedCache = distributedCache;
_configRepo = configRepo;
}
// 获取全局配置(从Redis缓存)
public async Task<GlobalConfigDto> GetGlobalConfigAsync()
{
// 1. 从Redis获取缓存(键是"GlobalConfig")
var cachedData = await _distributedCache.GetStringAsync("GlobalConfig");
if (!string.IsNullOrEmpty(cachedData))
{
// 缓存存在:反序列化为对象并返回
return JsonSerializer.Deserialize<GlobalConfigDto>(cachedData);
}
// 2. 缓存不存在:查数据库
var config = await _configRepo.GetAsync(x => x.IsDefault);
var configDto = ObjectMapper.Map<GlobalConfig, GlobalConfigDto>(config);
// 3. 存入Redis缓存(设置1小时过期)
await _distributedCache.SetStringAsync(
key: "GlobalConfig",
value: JsonSerializer.Serialize(configDto),
options: new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1) // 1小时后失效
}
);
return configDto;
}
}
配置Redis(appsettings.json):
"Redis": {
"Configuration": "localhost:6379", // Redis服务器地址
"InstanceName": "MyApp_" // 实例名(避免多个应用共用Redis时键冲突)
}
5. ICacheKeyNormalizer:避免缓存键冲突
在多租户系统或模块化应用中,不同租户/模块可能用相同的缓存键(如都用"Config"),导致冲突。ICacheKeyNormalizer会自动给缓存键加前缀(如租户ID、模块名)。
示例:自动添加租户前缀
ABP默认的CacheKeyNormalizer会处理多租户场景:
- 租户1的缓存键:
Tenant_1:UserCache:123; - 租户2的缓存键:
Tenant_2:UserCache:123; - 避免不同租户的数据互相干扰。
自定义缓存键前缀(可选):
public class MyCacheKeyNormalizer : CacheKeyNormalizerBase
{
public MyCacheKeyNormalizer(ICurrentTenant currentTenant)
: base(currentTenant)
{
}
public override string NormalizeKey(CacheKeyNormalizeContext context)
{
// 自定义前缀:模块名 + 租户ID + 原始键
return $"MyModule:{CurrentTenant.Id}:{context.Key}";
}
}
// 注册自定义缓存键标准化器(在模块中)
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.Replace(ServiceDescriptor.Singleton<ICacheKeyNormalizer, MyCacheKeyNormalizer>());
}
四、缓存的核心配置(appsettings.json)
"AbpCaching": {
"CacheExpirationDefaults": {
"Default": 60, // 默认缓存时间(分钟)
"Entity": 360, // 实体缓存默认时间(分钟)
"Permission": 360, // 权限缓存默认时间(分钟)
"Setting": 360, // 设置缓存默认时间(分钟)
"User": 120 // 用户信息缓存默认时间(分钟)
}
}
五、新手避坑指南
- 缓存数据必须可序列化:存入分布式缓存(如Redis)的数据需要能序列化为字符串(避免存入复杂对象如
DataTable); - 缓存失效策略要合理:
- 高频访问、低频修改的数据:缓存时间可以长(如1小时);
- 低频访问、高频修改的数据:不建议缓存(或缓存时间短,如1分钟);
- 避免缓存“热点数据”:如首页Banner、热门商品,更新时要及时用
[CacheInvalidate]删除缓存; - 分布式缓存配置:集群部署必须用
IDistributedCache(Redis),否则多服务器缓存不一致; - 不要缓存太大的数据:如整个表的10万条记录,会占用过多内存,建议分页缓存。
总结
- 简单场景:用
[Cache]+[CacheInvalidate],零代码实现缓存; - 灵活场景:用
ICacheManager手动控制缓存(如部分数据缓存); - 集群场景:用
IDistributedCache(Redis)实现缓存共享; - 多租户/模块化:依赖
ICacheKeyNormalizer避免键冲突。
通过合理使用缓存,能显著减少数据库压力,提升系统响应速度。需要“缓存与数据库一致性保障”的深入讲解可以告诉我。
(二)ABP 配置 Redis
抱歉之前没详细讲Redis配置!在ABP中使用分布式缓存(Redis)需要几个关键步骤,包括安装依赖、配置连接、注册服务,下面手把手你一步一步讲清楚,确保能跑通。
1、准备工作:安装Redis服务器
首先需要在你的服务器(或本地)安装Redis:
- Windows:推荐用 Redis for Windows(下载.msi文件安装,默认端口6379);
- Linux:直接用命令安装
sudo apt-get install redis-server; - 安装后启动Redis,默认不需要密码(生产环境需配置密码)。
2、ABP项目中配置Redis的完整步骤
步骤1:安装Redis依赖包
在你的 应用层项目(如MyApp.Application)或 Web层项目(如MyApp.Web)中,安装ABP的Redis缓存包:
# 通过NuGet安装(Package Manager控制台)
Install-Package Volo.Abp.Caching.StackExchangeRedis
# 或通过.NET CLI
dotnet add package Volo.Abp.Caching.StackExchangeRedis
步骤2:在模块中引入Redis模块
在你的 核心模块(如MyAppWebModule)中,通过[DependsOn]引入AbpCachingStackExchangeRedisModule,开启Redis缓存支持:
using Volo.Abp.Caching.StackExchangeRedis;
using Volo.Abp.Modularity;
[DependsOn(
typeof(AbpAspNetCoreModule),
typeof(AbpCachingStackExchangeRedisModule) // 引入Redis缓存模块
)]
public class MyAppWebModule : AbpModule
{
// 后续配置写在这里
}
步骤3:配置Redis连接(appsettings.json)
在appsettings.json中添加Redis连接配置,告诉ABP如何连接你的Redis服务器:
{
"Redis": {
"Configuration": "localhost:6379", // Redis服务器地址+端口(默认6379)
"InstanceName": "MyApp_" // 实例名(可选,用于区分多个应用的缓存键)
},
"AbpDistributedCacheOptions": {
"KeyPrefix": "MyAppCache_" // 缓存键统一前缀(可选,进一步避免冲突)
}
}
配置说明:
Configuration:Redis连接字符串,格式为"服务器地址:端口[,password=密码]";- 示例(带密码):
"192.168.1.100:6379,password=myredis123"; - 示例(集群):
"192.168.1.101:6379,192.168.1.102:6379";
- 示例(带密码):
InstanceName:多个应用共用一个Redis时,用实例名区分(如"ShopApp_"、"AdminApp_");KeyPrefix:所有缓存键会自动加上这个前缀(如"MyAppCache_User_123"),避免键重复。
步骤4:验证Redis是否生效
配置完成后,ABP会自动将IDistributedCache的实现替换为Redis缓存,无需额外代码。可以通过以下方式验证:
示例:存入缓存并在Redis中查看
using Microsoft.Extensions.Caching.Distributed;
using Volo.Abp.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
public class RedisTestController : AbpController
{
private readonly IDistributedCache _distributedCache;
public RedisTestController(IDistributedCache distributedCache)
{
_distributedCache = distributedCache;
}
// 访问此接口:将数据存入Redis
[HttpGet("set-cache")]
public async Task<IActionResult> SetCacheAsync()
{
// 存入缓存:键为"TestKey",值为"Hello Redis"
await _distributedCache.SetStringAsync(
"TestKey",
"Hello Redis",
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) // 10分钟后过期
}
);
return Ok("缓存已存入Redis");
}
// 访问此接口:从Redis读取缓存
[HttpGet("get-cache")]
public async Task<IActionResult> GetCacheAsync()
{
var value = await _distributedCache.GetStringAsync("TestKey");
return Ok($"从Redis读取到的值:{value}");
}
}
验证步骤:
- 启动项目,访问
https://localhost:端口/set-cache,返回“缓存已存入Redis”; - 打开Redis客户端(如Windows的
redis-cli.exe),输入命令keys *,会看到类似MyApp_MyAppCache_TestKey的键(前缀由配置的InstanceName和KeyPrefix组成); - 输入
get MyApp_MyAppCache_TestKey,会返回Hello Redis,说明缓存成功存入Redis; - 访问
https://localhost:端口/get-cache,会返回“从Redis读取到的值:Hello Redis”,验证读取成功。
步骤5:高级配置(可选)
如果需要更精细的Redis配置(如连接池大小、超时时间),可以在模块的ConfigureServices中手动配置:
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
// 手动配置Redis
Configure<AbpRedisCacheOptions>(options =>
{
// 从配置文件读取连接字符串
options.Configuration = configuration["Redis:Configuration"];
options.InstanceName = configuration["Redis:InstanceName"];
// 高级选项:连接池大小(默认50)
options.ConnectionPoolSize = 100;
// 连接超时时间(毫秒)
options.ConnectTimeout = 5000;
// 同步超时时间(毫秒)
options.SyncTimeout = 3000;
});
}
3、常见问题与解决办法
-
无法连接Redis:
- 检查
Configuration中的地址和端口是否正确(默认localhost:6379); - 确保Redis服务器已启动(Windows可在服务中查看“Redis”服务状态);
- 关闭防火墙或开放6379端口。
- 检查
-
缓存键重复:
- 务必配置
InstanceName和KeyPrefix,尤其是多个应用共用一个Redis时; - 多租户系统中,ABP的
ICacheKeyNormalizer会自动添加租户ID前缀,无需额外处理。
- 务必配置
-
缓存数据序列化问题:
-
IDistributedCache默认用JSON序列化,复杂对象(如DateTimeOffset)可能有兼容性问题; -
可自定义序列化器(如用Protobuf),提升性能和兼容性:
Configure<AbpDistributedCacheOptions>(options => { options.Serializer = new MyProtobufCacheSerializer(); // 自定义序列化器 });
-
通过以上步骤,你的ABP项目就能成功集成Redis分布式缓存了。分布式缓存特别适合集群部署(多台服务器共享缓存),或需要持久化缓存(避免应用重启后缓存丢失)的场景。

浙公网安备 33010602011771号