多级缓存
这页对应 3.x 里基于 Aegis.Caching.Redis 的多级缓存能力。它的核心思路是把本地内存缓存作为 L1,把 Redis 作为 L2,把数据库或真实业务方法作为 L3,并通过缓存拦截器把读写删除动作统一接起来。
先记住多级缓存的边界
当前这套能力不是通过 Component.deps.json 自动启用的,而是代码级接入。
要让它生效,至少要同时满足四件事:
- 已接入 Redis 数据源
- 已注册
MemoryCache - 已注册缓存拦截器
- 目标方法上已经打了缓存注解
什么时候适合用它
适合场景:
- 读多写少的基础资料、字典、主数据
- 查询频繁但变化不频繁的聚合结果
- 希望同一个实例命中本地缓存,多实例之间再通过 Redis 共享
不太适合一上来就接:
- 高频强一致写入
- 事务边界非常复杂的实时状态
- 还没想清楚失效策略的场景
整体结构
最常见的行为是:
Cacheable:先查L1,再查L2,都没有才执行真实方法CachePut:执行真实方法后回写缓存CacheEvict:在合适时机删除缓存
最小可运行路径
第一步:先接好 Redis
services.AddRedisSource<AegisRedisSource>(
ConfigManager.Get<RedisOptions>("Redis"));
第二步:注册多级缓存所需服务
services.AddMemoryCache();
services.AddSingleton<StrategyFactory>();
services.AddSingleton<CacheInterceptor<AegisRedisSource>>();
services.AddSingleton<RedisSourceBase>(sp => sp.GetRequiredService<AegisRedisSource>());
第三步:把缓存拦截器挂到动态代理
services.ConfigureDynamicProxy(options =>
{
options.Interceptors.AddTyped<CacheInterceptor<AegisRedisSource>>(
Predicates.ForService("*Service"));
options.Interceptors.AddTyped<CacheInterceptor<AegisRedisSource>>(
Predicates.ForService("*Repository"));
});
第四步:给目标方法加注解
[Cacheable(Domain = "User", Key = "{id}", TTL = 300, Jitter = 30, Strategy = CacheStrategyType.Simple)]
public Task<UserInfo> GetUserInfo(int id)
{
var userInfo = new UserInfo
{
Id = id,
Name = $"User_{id}"
};
return Task.FromResult(userInfo);
}
这组注解分别负责什么
| 注解 | 作用 | 常见场景 |
|---|---|---|
Cacheable | 读缓存,未命中时执行真实方法并回填 | 查询 |
CachePut | 执行方法后更新缓存 | 保存、修改 |
CacheEvict | 删除缓存 | 删除、失效 |
常见参数怎么理解
| 参数 | 作用 |
|---|---|
Domain | 缓存域,用于区分业务空间 |
Key | 缓存键模板,如 {id} |
TTL | 缓存时长,单位秒 |
Jitter | 抖动秒数,用于打散过期时间 |
Strategy | 缓存策略,常见是 Simple 或 Hash |
BeforeInvocation | CacheEvict 是否在方法执行前删除 |
Simple 策略怎么写
Simple 更适合普通对象缓存,也是最推荐的起步方式。
查询
[Cacheable(Domain = "User", Key = "{id}", TTL = 300, Jitter = 30, Strategy = CacheStrategyType.Simple)]
public Task<UserInfo> GetUserInfo(int id)
{
var userInfo = new UserInfo
{
Id = id,
Name = $"User_{id}"
};
return Task.FromResult(userInfo);
}
更新
[CachePut(Domain = "User", Key = "{id}", TTL = 300, Strategy = CacheStrategyType.Simple)]
public Task<UserInfo> SaveOrUpdateUserInfo(int id, UserInfo user)
{
return Task.FromResult(user);
}
删除
[CacheEvict(Domain = "User", Key = "{id}", Strategy = CacheStrategyType.Simple, BeforeInvocation = false)]
public Task<bool> DeleteUserInfo(int id)
{
return Task.FromResult(true);
}
Hash 策略怎么写
Hash 更适合按一个业务域挂多个字段值的场景,比如医生、号源、明细集合。
查询
[Cacheable(Domain = "Doctor", Key = "{id}", TTL = 300, Jitter = 30, Strategy = CacheStrategyType.Hash)]
public Task<DoctorInfo> GetDoctorInfo(int id)
{
var doctorInfo = new DoctorInfo
{
Id = id,
Name = $"Doctor_{id}",
Description = $"This is Doctor_{id} description.",
DateTime = DateTime.Now
};
return Task.FromResult(doctorInfo);
}
更新
[CachePut(Domain = "Doctor", Key = "{id}", TTL = 300, Strategy = CacheStrategyType.Hash)]
public Task<DoctorInfo> SaveOrUpdateDoctorInfo(int id, DoctorInfo doctorInfo)
{
return Task.FromResult(doctorInfo);
}
删除
[CacheEvict(Domain = "Doctor", Key = "{id}", Strategy = CacheStrategyType.Hash, BeforeInvocation = false)]
public Task<bool> DeleteDoctorInfo(int id)
{
return Task.FromResult(true);
}
Service 和 Repository 都能接吗
可以。
当前示例里两边都能挂,但落法不同。
Service 层
普通业务服务直接加注解即可。
[Cacheable(Domain = "User", Key = "{id}", TTL = 300, Jitter = 30, Strategy = CacheStrategyType.Simple)]
public Task<UserInfo> GetUserInfo(int id)
{
...
}
Repository 层
如果是仓储层方法,需要注意方法通常要声明成 virtual,这样代理拦截才容易生效。
[Cacheable(Domain = "DockerInfo", Key = "{id}", TTL = 300, Jitter = 30, Strategy = CacheStrategyType.Simple)]
public virtual async Task<DockerInfoEntity> GetDockerInfoEntityById(int id)
{
return await _db.Select<DockerInfoEntity>()
.Where(a => a.Id == id)
.ToOneAsync();
}
L1Cache 和 Jitter 为什么重要
L1Cache.Enabled
控制是否启用本地内存缓存。
开启后,同一个实例内会先命中内存,性能更高。
{
"Redis": {
"L1Cache": {
"Enabled": true
}
}
}
Jitter
用于给过期时间加一点随机抖动,避免大量键在同一秒集中过期。
[Cacheable(Domain = "User", Key = "{id}", TTL = 300, Jitter = 30, Strategy = CacheStrategyType.Simple)]
这表示实际过期时间会落在 300 ~ 330 秒之间。
接入完成后怎么确认成功
你至少应该确认这些点:
- Redis 和
MemoryCache都已注册 StrategyFactory和CacheInterceptor<T>已注册ConfigureDynamicProxy(...)已把拦截器挂到目标服务- 方法上已经打了
Cacheable / CachePut / CacheEvict - 重复请求同一数据时,真实方法不会每次都执行
常见问题
为什么打了注解,但缓存完全没生效
优先检查:
- 是否注册了
CacheInterceptor<T> - 是否配置了
ConfigureDynamicProxy(...) - 目标类名是否匹配
*Service或*Repository
为什么 Repository 层打了注解还是没拦截
优先检查方法是不是 virtual。
如果不是 virtual,仓储层代理通常就挂不上去。
为什么更新后还是读到旧值
最常见原因是:
- 更新方法没有打
CachePut - 删除方法没有打
CacheEvict Domain或Key前后写得不一致
为什么不建议一开始就给所有服务都加缓存
因为缓存命中、失效、一致性和异常恢复都要配套设计。
最稳的做法是先从读多写少、键模型清晰的一小组服务开始。