缓存
本页将帮助你快速了解 Ape‑Volo‑Admin 的缓存使用与配置。
缓存实现一览
系统内置两种缓存实现:
- DistributedCache:进程内内存缓存(本地缓存),部署单实例或开发调试场景下更轻量。
- RedisCache:基于 Redis 的分布式缓存,适用于多实例/横向扩展与跨进程共享缓存。
选择开关:当 SystemOptions.UseRedisCache = true 时启用 RedisCache,否则使用 DistributedCache。
- 系统同时使用两种缓存方案扩展了对
SqlSugar的二级扩展,按需使用。
AOP 缓存 开关
- 在配置中开启:
Aop.Cache.Enabled = true(参见“配置文档 > AOP 配置”)。 - 开启后,被缓存特性标记的方法会被拦截并自动进行缓存处理。
🧱 缓存实现与选择
- 单机开发/功能调试:优先使用 DistributedCache,零依赖,成本低。
- 多节点部署/需要共享会话或跨进程复用数据:使用 RedisCache,具备更强的可扩展性与可观测性。
- 对一致性要求高的强一致写场景:建议缩短 TTL + 走“读穿透”策略,或仅在读多写少的接口开启缓存。
🔧 Redis 配置
配置文件设置
建议在 appsettings.Development.json 中配置 Redis 相关参数(生产环境在对应环境文件覆盖):
yaml
Redis:
Name: "" # Redis 连接名称
Host: "localhost" # Redis 主机地址
Port: 6379 # Redis 端口
Password: "" # Redis 密码
Index: 0 # 默认数据库索引
ConnectTimeout: 10000 # 连接超时时间(毫秒)
SyncTimeout: 10000 # 同步操作超时(毫秒)
KeepAlive: 20 # 保持活动时间(秒)
ConnectRetry: 10 # 连接重试次数
AbortOnConnectFail: true # 连接失败是否中止
AllowAdmin: true # 是否允许执行管理命令
SuspendTime: 10000 # 挂起时间(毫秒)
IntervalTime: 0 # 调度间隔时间(毫秒)
MaxQueueConsumption: 100 # 最大队列消费数量
ShowLog: false # 是否显示日志缓存注册
位置:Ape.Volo.Infrastructure/Extensions/CacheExtensions.cs
csharp
/// <summary>
/// 缓存启动器
/// </summary>
public static class CacheExtensions
{
public static void AddCacheService(this IServiceCollection services)
{
if (services.IsNull())
throw new ArgumentNullException(nameof(services));
services.AddDistributedMemoryCache(); //session 需要
if (App.GetOptions<SystemOptions>().UseRedisCache)
{
services.AddSingleton<ICache, RedisCache>();
return;
}
services.AddSingleton<ICache, DistributedCache>();
}
}说明:
- 始终注册内存缓存(支持会话/短期缓存)。
- 根据
UseRedisCache选择 ICache 的具体实现,业务代码通过 ICache 抽象无感知切换。
🧩 键设计与过期策略
合理的 Key 设计与过期策略是缓存命中与稳定性的关键:
- Key 规范:
模块:资源:维度,如:dict:byName:{name};尽量使用短、稳定且可读的分隔符(:)。 - KeyPrefix:通过
UseCacheAttribute.KeyPrefix统一前缀,便于批量定位与清理。 - TTL(过期时间):根据数据的“更新频率 × 容忍滞后度”确定;可引入抖动(随机 ±10%)避免雪崩。
- 过期模型:
- 绝对过期(默认):到时即失效,最简单;
- 滑动过期:活跃数据持续续期,适合短期热点但需注意长期不失效的问题。
缓存 AOP
位置:Ape.Volo.Core/Aop/CacheAop.cs
csharp
/// <summary>
/// Redis特性 AOP拦截使用
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class UseCacheAttribute : Attribute
{
/// <summary>
/// 过期时间(分钟)
/// </summary>
public int Expiration { get; set; } = 20;
/// <summary>
/// 缓存key前缀
/// </summary>
public string KeyPrefix { get; set; } = "";
/// <summary>
/// 缓存类型(默认绝对过期)
/// </summary>
public CacheExpireType CacheExpireType { get; set; } = CacheExpireType.Absolute;
}
/// <summary>
/// Redis缓存拦截器
/// </summary>
public class CacheAop : IInterceptor
{
private readonly ICache _cache;
public CacheAop(ICache cache)
{
_cache = cache;
}
public void Intercept(IInvocation invocation)
{
var method = invocation.MethodInvocationTarget ?? invocation.Method;
if (method.ReturnType == typeof(void) || method.ReturnType == typeof(Task))
{
invocation.Proceed();
return;
}
if (method.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(UseCacheAttribute)) is
UseCacheAttribute useCacheAttribute)
{
var cacheKey = CreateCacheKey(invocation, useCacheAttribute);
var cacheValue = _cache.Get<dynamic>(cacheKey);
if (cacheValue != null)
{
dynamic result = cacheValue;
invocation.ReturnValue = typeof(Task).IsAssignableFrom(method.ReturnType)
? Task.FromResult(result)
: result;
return;
}
invocation.Proceed();
if (!string.IsNullOrWhiteSpace(cacheKey))
{
object response = null;
var type = invocation.Method.ReturnType;
if (typeof(Task).IsAssignableFrom(type))
{
var resultProperty = type.GetProperty("Result");
if (resultProperty != null) response = resultProperty.GetValue(invocation.ReturnValue);
}
else
{
response = invocation.ReturnValue;
}
if (response.IsNullOrEmpty())
{
return;
}
_cache.Set(cacheKey, response, TimeSpan.FromMinutes(useCacheAttribute.Expiration),
null);
}
}
else
{
invocation.Proceed();
}
}
/// <summary>
/// 构建Redis Key
/// </summary>
/// <param name="invocation"></param>
/// <param name="useCacheAttribute"></param>
/// <returns></returns>
private static string CreateCacheKey(IInvocation invocation, UseCacheAttribute useCacheAttribute)
{
var typeName = invocation.TargetType.Name;
var methodName = invocation.Method.Name;
//支持多参数包括实体类,建议不要超过三个, 避免产生的redis key过长
var methodArguments = invocation.Arguments.Select(GetArgumentValue).ToList();
var key = useCacheAttribute.KeyPrefix.IsNullOrEmpty()
? $"{typeName}:{methodName}:"
: useCacheAttribute.KeyPrefix;
methodArguments.ForEach(arg => { key = $"{key}{arg}:"; });
return key.TrimEnd(':');
}
/// <summary>
/// 获取参数值类型值
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
private static string GetArgumentValue(object arg)
{
if (arg == null) return string.Empty;
switch (arg)
{
case string:
case long:
return arg.ToString().ToMd5String16();
case DateTime:
return arg.ToString("yyyyMMddHHmmss").ToMd5String16();
default:
{
if (arg.GetType().IsClass)
{
return arg.ToJson().ToMd5String16();
}
return string.Empty;
}
}
}
}缓存使用
在需要缓存的方法上添加 [UseCache] 特性:
csharp
[UseCache(Expiration = 30, KeyPrefix = GlobalConstants.CachePrefix.LoadDictByName)]
public async Task<DictVo> QueryByNameAsync(string name)
{
var dict = await Table.Where(x => x.Name == name).Includes(x => x.DictDetails.OrderBy(y => y.DictSort)
.ToList()).FirstAsync();
return App.Mapper.MapTo<DictVo>(dict);
}失效与刷新示例
当写操作导致数据变化时,应及时失效相关 Key,避免读到旧数据:
csharp
// 更新字典后,移除相关缓存
await _cache.RemoveAsync($"{GlobalConstants.CachePrefix.LoadDictByName}:{name}");
// 或采用“写后刷新”的方式:更新成功后,重新设置最新值(小心并发与事务一致性)🧭 何时适合使用缓存
- 读多写少的接口:例如配置、字典、热门列表、统计汇总等。
- 计算/聚合成本高:复杂查询、跨服务聚合、第三方接口结果等。
- 跨请求复用:同一数据被多个接口或多个用户频繁访问。
不建议使用的场景:
- 对实时性/一致性要求极高的强一致写读链路(例如支付扣款后的余额实时展示)。
- 数据体量巨大且低复用、或个别超大 Value(>1MB)频繁写入。
⚠️ 常见风险与规避
- 缓存击穿(热点 Key 过期瞬间被大量并发请求穿透)
- 规避:热点 Key 设置较长 TTL + 提前异步刷新;用互斥锁/单飞阀(SingleFlight)保护回源。
- 缓存穿透(请求的数据不存在,持续打到 DB)
- 规避:对不存在的结果也缓存短 TTL 的空值;布隆过滤器预判非法 Key。
- 缓存雪崩(大量 Key 在同一时刻同时过期)
- 规避:TTL 加随机抖动;错峰失效;必要时分批预热。
- 数据不一致(写成功但缓存未失效或失效慢)
- 规避:写操作后先删缓存再写库或使用可靠地“写后删/写后刷新”策略;缩短 TTL;必要时引入消息驱动的失效。
- 热点 Key 与大 Value
- 规避:拆分热点 Key 或采用本地 + 远端两级缓存;对大对象做字段级缓存或分页缓存。
- 序列化与兼容
- 规避:统一序列化协议(如 Newtonsoft.Json),避免不同版本模型不兼容。
- 连接与资源
- 规避:合理配置连接池与超时,监控 QPS、慢查询、内存压力;避免在热点路径执行阻塞命令。
✅ 小结
- 通过 ICache 抽象无缝切换 DistributedCache/RedisCache。
- 用好
UseCacheAttribute+CacheAop,配合合理 Key/TTL 设计,能显著提升读性能。 - 关注击穿/穿透/雪崩与一致性问题,按需引入抖动、互斥与空值缓存等手段。

