跳到主要内容
版本:3.0.0

多级缓存

这页对应 3.x 里基于 Aegis.Caching.Redis 的多级缓存能力。它的核心思路是把本地内存缓存作为 L1,把 Redis 作为 L2,把数据库或真实业务方法作为 L3,并通过缓存拦截器把读写删除动作统一接起来。

先记住多级缓存的边界

当前这套能力不是通过 Component.deps.json 自动启用的,而是代码级接入。
要让它生效,至少要同时满足四件事:

  1. 已接入 Redis 数据源
  2. 已注册 MemoryCache
  3. 已注册缓存拦截器
  4. 目标方法上已经打了缓存注解

什么时候适合用它

适合场景:

  • 读多写少的基础资料、字典、主数据
  • 查询频繁但变化不频繁的聚合结果
  • 希望同一个实例命中本地缓存,多实例之间再通过 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缓存策略,常见是 SimpleHash
BeforeInvocationCacheEvict 是否在方法执行前删除

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);
}

ServiceRepository 都能接吗

可以。
当前示例里两边都能挂,但落法不同。

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();
}

L1CacheJitter 为什么重要

L1Cache.Enabled

控制是否启用本地内存缓存。
开启后,同一个实例内会先命中内存,性能更高。

{
"Redis": {
"L1Cache": {
"Enabled": true
}
}
}

Jitter

用于给过期时间加一点随机抖动,避免大量键在同一秒集中过期。

[Cacheable(Domain = "User", Key = "{id}", TTL = 300, Jitter = 30, Strategy = CacheStrategyType.Simple)]

这表示实际过期时间会落在 300 ~ 330 秒之间。

接入完成后怎么确认成功

你至少应该确认这些点:

  • Redis 和 MemoryCache 都已注册
  • StrategyFactoryCacheInterceptor<T> 已注册
  • ConfigureDynamicProxy(...) 已把拦截器挂到目标服务
  • 方法上已经打了 Cacheable / CachePut / CacheEvict
  • 重复请求同一数据时,真实方法不会每次都执行

常见问题

为什么打了注解,但缓存完全没生效

优先检查:

  1. 是否注册了 CacheInterceptor<T>
  2. 是否配置了 ConfigureDynamicProxy(...)
  3. 目标类名是否匹配 *Service*Repository

为什么 Repository 层打了注解还是没拦截

优先检查方法是不是 virtual
如果不是 virtual,仓储层代理通常就挂不上去。

为什么更新后还是读到旧值

最常见原因是:

  • 更新方法没有打 CachePut
  • 删除方法没有打 CacheEvict
  • DomainKey 前后写得不一致

为什么不建议一开始就给所有服务都加缓存

因为缓存命中、失效、一致性和异常恢复都要配套设计。
最稳的做法是先从读多写少、键模型清晰的一小组服务开始。

推荐阅读顺序

  1. Redis
  2. Redis 缓存(Aegis.Caching.Redis)
  3. 配置