多级缓存一致性矩阵:ABP vNext 下的旁路 / 写穿 / 写回组合实战
📚 目录
1. 概念速览与边界
- 多级缓存:L1 进程内
IMemoryCache🔐;L2 分布式IDistributedCache/Redis🧑💻;L3 边缘(CDN/反代) 🌐。 - 写策略:旁路(Cache-Aside) 🔄、写穿(Write-Through) 📝、写回/写后(Write-Back/Behind) 🔄。
- 一致性指标:读你所写(强一致性/同步) ⚖️、软实时(≤N 秒追平) ⏱️、最终一致 🔁。
- 本文关注策略组合与治理,讨论如何在多租户 SaaS 环境下,合理配置缓存,提升系统的可用性与性能 ⚡。
2. 体系结构

- 读路径:L1 命中 → 否则查 L2 → 再否回源并回写 L2/L1(带 SWR 与抖动)。SWR=先返回陈旧值,后台重验证。
- 写路径:根据业务在旁路/写穿/写回之间选择;多实例间靠 Redis Pub/Sub 做 L1 失效广播。
- 多租户:ABP 支持多租户隔离,建议采用 租户隔离的 Key 空间与 TTL 策略,保证不同租户缓存不受影响。
3. Key 规范与租户维度 TTL
Key 模板:{app}:{env}:{tenant}:{entity}:{id}(ABP 统一管理缓存前缀)
TTL 计算:TTL = BaseTTL(tenant,entity) × Jitter(±10~25%)(加 TTL 抖动 以减少雪崩效应)。
策略提供器接口(放入 ABP 模块中):
public interface ICachePolicyProvider {
CachePolicy Get(string tenant, string entity); // 包含 hardTtl, softTtl, jitter, negativeTtl 等
}
public record CachePolicy(TimeSpan HardTtl, TimeSpan SoftTtl, double JitterPct,
TimeSpan NegativeTtl, bool EnableSWR, bool EnableBloom);
4. 三种写法的组合与适用
| 写法 | 一致性 | 典型风险 | 适用场景 |
|---|---|---|---|
| 旁路 Cache-Aside | 软实时/最终一致 | 击穿/回源洪峰 | 读多写少,允许轻微延迟 |
| 写穿 Write-Through | 较强一致性 | 写延迟变大 | 单体/中低写压、需强一致性 |
| 写回 Write-Back/Behind | 低延迟/高吞吐 | 数据丢失/回放复杂 | 高写入、可容忍短暂不一致 |
组合建议:
- 读多写少:旁路 + SWR + Single-Flight + 负缓存
- 中等写压、需读你所写:写穿 + L1 失效广播
- 写峰值极高:写回(Redis Streams/队列 + 持久化日志)+ 回放与审计
5. 雪崩 / 穿透 / 击穿 治理
- 雪崩:通过 TTL 抖动、SWR 软过期、热点预热、分布式锁保护回源。
- 穿透:RedisBloom(布隆过滤器)+ 负缓存(短 TTL)+ 参数白名单。
- 击穿:Single-Flight(同 key 回源去重)+ 后台刷新。
Single-Flight(L2 保护示例)
public sealed class SingleFlight {
private readonly ConcurrentDictionary<string, SemaphoreSlim> _locks = new();
public async Task<T> RunAsync<T>(string key, Func<Task<T>> loader) {
var gate = _locks.GetOrAdd(key, _ => new SemaphoreSlim(1, 1));
await gate.WaitAsync();
try {
return await loader();
} finally {
gate.Release();
_locks.TryRemove(key, out _);
}
}
}
6. SWR:Soft-Expire
- 逻辑:当 SoftTTL 超过但 HardTTL 未到时,返回陈旧值并后台刷新数据。
- 优点:有效削峰、改善尾延迟;缺点:短时间内读到陈旧数据。
SWR 实现骨架(Redis + 单飞 + 失效广播)
public async Task<T?> GetWithSwrAsync<T>(
string key, Func<Task<T>> dbLoader, CachePolicy p,
IDistributedCache cache, SingleFlight sf, ISubscriber pub, string? invChannel = null)
{
var bytes = await cache.GetAsync(key);
var entry = bytes?.FromBytes<CacheEnvelope<T>>();
// 新鲜
if (entry is {
} && entry.SoftExpireAt > DateTimeOffset.UtcNow)
return entry.Value;
// 软过期:先旧后新
if (entry is {
} && entry.HardExpireAt > DateTimeOffset.UtcNow) {
_ = Task.Run(async () => {
var v = await sf.RunAsync(key, dbLoader, CancellationToken.None);
await cache.SetAsync(key, new CacheEnvelope<T>(v, p).ToBytes(), new DistributedCacheEntryOptions {
AbsoluteExpirationRelativeToNow = p.HardTtl.WithJitter(p.JitterPct)
});
if (invChannel is not null) await pub.PublishAsync(invChannel, key);
});
return entry.Value;
}
// 完全过期:受单飞保护
var fresh = await sf.RunAsync(key, dbLoader);
await cache.SetAsync(key, new CacheEnvelope<T>(fresh, p).ToBytes(),
new DistributedCacheEntryOptions {
AbsoluteExpirationRelativeToNow = p.HardTtl.WithJitter(p.JitterPct) });
return fresh;
}
public readonly record struct CacheEnvelope<T>(T Value, DateTimeOffset SoftExpireAt, DateTimeOffset HardExpireAt) {
public CacheEnvelope(T value, CachePolicy p) : this(
value,
DateTimeOffset.UtcNow + p.SoftTtl

最低0.47元/天 解锁文章
2010

被折叠的 条评论
为什么被折叠?



