概述
silky 的缓存拦截器(Caching Interceptor)提供了一种基于 AOP 的透明缓存机制:开发者只需在服务接口方法上标注特定特性,框架即可在方法调用时自动完成缓存的读取、更新和删除,无需在业务代码中编写缓存逻辑。
缓存拦截器底层依赖 IDistributedInterceptCache(默认基于 Redis 实现),通过 CachingInterceptor(实现了 SilkyInterceptor)拦截服务方法调用并完成缓存操作。
启用条件
缓存拦截器由以下两个条件共同控制:
- 服务治理选项中
EnableCachingInterceptor = true(默认值为true) - 服务方法上至少标注了一个缓存拦截特性
# appsettings.yml
Governance:
EnableCachingInterceptor: true
如果某个方法不希望缓存拦截器生效,可以在该方法的 [Governance] 特性中单独关闭:
[Governance(EnableCachingInterceptor = false)]
Task<AccountDto> GetSensitiveDataAsync(long id);
缓存拦截特性
silky 提供了四种缓存拦截特性,分别对应不同的缓存操作:
GetCachingIntercept(读取缓存)
[GetCachingIntercept("{id}")]
Task<AccountDto> GetByIdAsync(long id);
行为:先查询缓存,命中则直接返回缓存值,不执行方法;未命中则执行方法,并将结果写入缓存。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
KeyTemplate(构造参数) | string | — | 缓存键模板,使用 {参数名} 占位符引用方法参数 |
OnlyCurrentUserData | bool | false | 为 true 时,缓存键自动追加当前用户标识,实现用户级数据隔离 |
IgnoreMultiTenancy | bool | false | 为 true 时,忽略多租户隔离,不同租户共享同一缓存 |
UpdateCachingIntercept(更新缓存)
[UpdateCachingIntercept("{id}")]
Task<AccountDto> UpdateAsync(long id, UpdateAccountInput input);
行为:执行方法后,直接将新的返回値覆写到缓存中(SetAsync)。若缓存键中有参数値为 null 且 IgnoreWhenCacheKeyNull = true,则改为删除该缓存条目。支持在同一方法上标注多个 [UpdateCachingIntercept](AllowMultiple = true)。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
KeyTemplate(构造参数) | string | — | 缓存键模板 |
OnlyCurrentUserData | bool | false | 用户级数据隔离 |
IgnoreMultiTenancy | bool | false | 多租户隔离控制 |
IgnoreWhenCacheKeyNull | bool | true | 为 true 时,若缓存键解析结果为空则忽略该缓存操作;为 false 则抛出异常 |
RemoveCachingIntercept(删除缓存)
// 通过 CacheName 字符串指定缓存分区
[RemoveCachingIntercept("Account", "{id}")]
Task DeleteAsync(long id);
// 通过类型自动推断 CacheName(取类型的短名)
[RemoveCachingIntercept(typeof(GetCachingInterceptAttribute), "{id}")]
Task DeleteAsync(long id);
行为:执行方法后,根据指定的 CacheName 和 KeyTemplate 删除对应的缓存条目。支持多个 [RemoveCachingIntercept]。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
CacheName(构造参数) | string | — | 缓存分区名称(类似 Redis Hash 的 Hash Key) |
KeyTemplate(构造参数) | string | — | 缓存键模板 |
OnlyCurrentUserData | bool | false | 用户级数据隔离 |
IgnoreMultiTenancy | bool | false | 多租户隔离控制 |
RemoveMatchKeyCachingIntercept(模式匹配删除缓存)
// 删除所有以 "Account:" 开头的缓存键
[RemoveMatchKeyCachingIntercept("Account", "Account:*")]
Task BatchUpdateAsync(BatchUpdateInput input);
行为:使用正则或通配符模式匹配缓存键,批量删除匹配的缓存条目。适用于批量操作后清理相关缓存。
缓存键(KeyTemplate)解析
缓存键通过 CacheKeyHelper.GetCachingInterceptKey() 解析生成,支持两种模式:
参数名占位符
使用 {参数名} 引用方法参数:
// 方法签名:Task<AccountDto> GetByIdAsync(long id)
// KeyTemplate: "{id}"
// 生成的缓存键:Account:123(CacheName:实际值)
[HashKey] 属性占位符
当参数是复杂对象时,使用 [HashKey] 标记对象属性:
public class QueryInput
{
[HashKey]
public long Id { get; set; }
public string Name { get; set; }
}
// KeyTemplate: "{queryInput}"
// 实际取 queryInput.Id 的值作为缓存键
框架内部通过 CacheKeyType 区分这两种情况:
CacheKeyType | 含义 |
|---|---|
Parameter | 直接使用参数值(或其序列化结果)作为 Key |
Attribute | 读取对象中被 [HashKey] 标注的属性作为 Key |
缓存名(CacheName)
缓存名(CacheName)类似命名空间,将同类数据的缓存条目组织在一起,避免键冲突:
GetCachingInterceptAttribute和UpdateCachingInterceptAttribute的CacheName由框架根据接口类型自动推断(通常为接口短名,例如IAccountAppService→Account)RemoveCachingInterceptAttribute需要显式指定CacheName,以便跨方法清理同一分区的缓存
多租户隔离
当应用启用了多租户时,缓存拦截器默认在缓存键中追加当前租户 ID,实现租户间的缓存隔离:
缓存键格式:{CacheName}:{TenantId}:{UserId}:{KeyValue}
IgnoreMultiTenancy = true:不追加租户 ID,不同租户共享同一缓存(适用于公共数据)OnlyCurrentUserData = true:追加当前用户 ID,实现用户级隔离(适用于个人数据)
分布式事务场景中的缓存
当服务方法同时开启了缓存拦截和分布式事务时,CachingInterceptor 会跳过 Get 缓存逻辑(不从缓存读取),直接执行方法,以确保事务中读取到的是最新的数据库数据:
// CachingInterceptor 内部逻辑
if (serviceEntryDescriptor.IsDistributeTransaction)
{
// 分布式事务中跳过读缓存,直接执行方法
await InvocationProceedAsync(invocation);
}
else
{
// 非分布式事务:正常走缓存读取逻辑
invocation.ReturnValue = await GetResultFirstFromCache(cacheName, cacheKey);
}
执行流程
调用方(HTTP 请求 / RPC 调用)
│
▼
CachingInterceptor.InterceptAsync()
│
├── GovernanceOptions.EnableCachingInterceptor == false
│ → 直接执行方法(bypass)
│
├── 存在 [GetCachingIntercept]
│ │
│ ├── 查询缓存(_distributedCache.GetOrAddAsync)
│ │ │
│ │ ├── 命中:直接返回缓存值,不执行方法
│ │ └── 未命中:执行方法 + 写入缓存
│ │
├── 存在 [UpdateCachingIntercept](可多个)
│ │
│ └── 执行方法后:删除旧缓存 + SetAsync 写入新值
│
└── 存在 [RemoveCachingIntercept] / [RemoveMatchKeyCachingIntercept](可多个)
│
└── 执行方法后:RemoveForInterceptAsync 删除匹配缓存
使用示例
以账户服务为例:
/// <summary>
/// 查询:读取缓存,命中则跳过方法执行
/// </summary>
[GetCachingIntercept("{id}")]
Task<AccountDto> GetByIdAsync(long id);
/// <summary>
/// 更新:执行方法后更新缓存
/// </summary>
[UpdateCachingIntercept("{id}")]
Task<AccountDto> UpdateAsync(long id, UpdateAccountInput input);
/// <summary>
/// 删除:执行方法后清理对应缓存
/// </summary>
[RemoveCachingIntercept("Account", "{id}")]
Task DeleteAsync(long id);
/// <summary>
/// 多缓存联动:删除主数据缓存 + 列表缓存
/// </summary>
[RemoveCachingIntercept("Account", "{input.Id}")]
[RemoveCachingIntercept("AccountList", "*")]
Task<AccountDto> UpdateStatusAsync(UpdateStatusInput input);
与 IDistributedInterceptCache
IDistributedInterceptCache 是缓存拦截器使用的缓存接口,其默认实现基于 IDistributedCache(Redis):
| 方法 | 说明 |
|---|---|
GetOrAddAsync(key, factory) | 读取缓存,未命中时调用 factory 写入 |
SetAsync(key, value) | 写入缓存 |
RemoveForInterceptAsync(key, cacheName, isMatchKey) | 删除缓存,isMatchKey=true 时按模式批量删除 |
UpdateCacheName(name) | 设置当前操作的缓存分区 |
SetIgnoreMultiTenancy(ignore) | 控制当前操作是否忽略多租户隔离 |
缓存拦截器在 Redis 中的存储结构:
- Key 格式:
{AppPrefix}:{CacheName}:{TenantId}:{ExtraKey} - Value:方法返回值的 JSON 序列化结果
