缓存读修复适合解决什么问题?
简化版
缓存读修复是在读取时发现缓存缺失、过旧或版本不一致后,从权威数据源读取并修正缓存,适合可容忍短暂陈旧、读多写少的派生缓存。它不能单独保证强一致,也不能替代写路径失效、事件同步和后台对账;热点 miss 还必须防并发回源与旧值晚写回。
详细版
读修复触发条件可以是 miss、逻辑过期、value 中版本低于请求已知版本,或双读抽检发现差异。流程是读取缓存 → 判断新鲜度 → 获取回源配额/同 Key 合并 → 读取数据库 → 带版本条件写缓存 → 返回结果。缓存不是权威源,修复失败通常不应反向覆盖数据库。
它的优势是只修真正被访问的数据,适合长尾 Key;缺点是第一次读延迟高、冷 Key 永不修复,故障时会把流量集中到数据库。应配合 singleflight、限流、随机 TTL、异步刷新和后台扫描。删除场景需 tombstone/版本防止旧查询把已删除数据重新写回。
读修复的一致性上限取决于权威读取与版本协议。若从延迟从库读取,修复可能仍是旧值;若无 CAS,慢请求可能覆盖更新。关键数据要读主/线性读或使用业务版本,并监控修复率、失败、回源和版本倒退拒绝。
完整版教学
一、读修复的定位
缓存是权威数据的可重建副本,读修复利用真实访问机会纠正副本。与写后主动删除相比,它发生在读路径;与后台全量校验相比,它只处理被访问 Key,因此天然把资源用在热数据上。
它适合商品详情、用户资料视图等允许短暂旧值的数据,不适合把余额、锁或唯一库存的正确性押在“下次有人读时再修”。强不变量仍应由数据库/一致性存储维护。
记忆钩子:读修复是最终一致的自愈手段,不是把缓存升级成权威数据库。
二、完整读取流程
缓存 value 至少带业务数据版本和生成时间。客户端命中后判断 TTL/逻辑过期与最低可接受版本;需要修复时先合并同 Key 请求,再读取权威源。写回使用版本条件,保证旧修复不能覆盖新缓存。
GET cache
├─ fresh 且 version 足够 -> 返回
└─ miss/stale -> singleflight + 回源配额
-> 读权威数据(version=v18)
-> 若 cache.version <= v18 才 SET
-> 返回权威结果
调用方若已知自己刚写到 v19,应携带最低版本 19,防止路由到旧缓存后状态倒退。
三、并发回源为什么会击穿
热点 Key 过期时,1000 个请求可能同时发现 stale 并查询数据库。singleflight 让单进程只执行一次加载,但 100 个应用实例仍可能产生 100 次;全局互斥、逻辑过期后台刷新或数据库回源限流可进一步保护。
等待修复的请求要有 deadline。若修复需要 500ms 而用户只剩 100ms,可按业务返回 stale,或明确失败;不能让等待队列无限增长。加载失败可短暂缓存错误/空值并退避,避免每个请求立即再试。
假设热点 2 万 QPS、100 实例,本地 singleflight 把一次瞬时回源从 2 万降到约 100,仍需确认数据库是否承受 100 并发。
四、旧值晚写回怎样发生
请求 A 在 t1 读取数据库 v10,随后网络卡住;请求 B 在 t2 更新数据库到 v11并刷新缓存;A 在 t3 恢复并无条件 SET v10,缓存从新值倒退。普通 TTL 无法防这个竞态。
t1 A: DB read v10 ---------------------+
t2 B: DB write v11 -> cache SET v11 |
t3 A: SET v10 <-+ 错误
解决方案是 value 带 version,并用 Lua/CAS 只允许不旧于当前版本的写;或使用版本化 Key。仅“先删后写”仍可能竞态,关键是可比较版本。
五、删除与负缓存的修复
数据库记录已删除时,回源返回不存在。若简单不写缓存,攻击或热点不存在 Key 会持续穿透;可写短 TTL 的 negative cache/tombstone,包含删除版本。后续若同 ID 合法重建,新版本必须能覆盖 tombstone。
| 回源结果 | 修复动作 | 注意点 |
|---|---|---|
| 存在且版本更新 | 条件 SET 新值 | 防旧写覆盖 |
| 存在但缓存更新 | 不写或刷新 TTL | 不降低版本 |
| 明确不存在 | 写短 TTL tombstone | 防穿透与误复活 |
| 权威源超时 | 不判定不存在 | 可 stale/失败 |
数据库超时绝不能当作“不存在”写负缓存,否则会制造错误否定结果。
六、从库读取的边界
若修复为了减轻主库压力而读异步从库,从库可能落后,导致刚更新的数据被旧值修复回缓存。可以要求修复读主库、读取达到某 GTID/LSN 的副本,或比较业务版本;不能只假设从库“通常很快”。
对可容忍旧值的普通详情,读从库加短 TTL可能足够;对用户写后立即读取,应读主或使用读己之写 token。方案选择必须写出最大复制滞后与业务容忍度。
读修复流量也要单独限额,避免主库故障时请求改打从库又将其压垮。
七、为什么还需要主动失效和后台修复
冷 Key 没有读就永远不会修;热点第一次读承担额外延迟;大规模缓存污染时,在线修复可能淹没数据库。因此写路径仍应主动删除/更新,CDC 负责可靠传播,读修复作为漏网兜底,后台对账处理长期差异。
监控缓存 miss、stale 命中、修复尝试/成功/失败、singleflight 合并、回源拒绝、版本倒退阻止和 tombstone 命中。抽样双读可估计未被发现的陈旧率,但要控制数据库成本。
read_repair_qps: 420
repair_success_rate: 99.8%
coalesced_requests: 18400
stale_write_rejected: 27
tombstone_hits: 931
origin_rate_limited: 4
八、常见误区与追问
- 误区:读修复可以替代所有缓存一致性机制。 冷 Key 不会被修,首次读有延迟,还可能造成回源洪峰,应作为兜底。
- 误区:回源超时等于数据不存在。 超时是未知,写负缓存会把真实数据错误隐藏。
- 误区:singleflight 后就没有击穿。 它常只在单进程合并,多实例仍需全局容量保护。
- 误区:从库也有数据,所以适合所有修复读取。 复制延迟会把旧值重新写回,高风险场景需读主或版本约束。
- 追问:如何防止旧查询晚写回? 缓存 value 带业务版本,通过 Lua/CAS 只接受不旧于当前版本的写。
- 追问:删除数据如何修复? 使用带删除版本和短 TTL 的 tombstone,防穿透与旧值复活。
- 追问:读修复失败要阻断请求吗? 取决于数据风险;可用 stale 的场景返回带标记旧值,高风险场景明确失败。
九、加强记忆
读修复按“判新鲜、合并回源、读权威、版本写回、主动机制兜底”执行。它只修被访问的缓存,必须限制回源并防旧值晚写;不存在要用 tombstone,超时不能当不存在。把它放在主动失效、CDC 和后台对账之后,才能形成可靠的最终一致闭环。