缓存一致性
缓存与数据库双写的一致性问题:读写都走两套存储,顺序不对就出现脏数据。面试主线:先讲清楚为什么难,再讲 Cache Aside 和它的竞态,再给工程兜底方案。
为什么难
数据库和缓存是两个独立系统,没有分布式事务。任何“先 A 后 B”的操作都存在中间态:
- 写库成功、写缓存失败 → 缓存是旧值
- 删缓存成功、写库失败 → 下次读会 miss,重新读库,问题不大
- 并发读写交叉 → 可能把旧值写回缓存
所以核心结论先行:做不到强一致,目标是最终一致,缩小不一致窗口。
Cache Aside(旁路缓存)
读流程:先查缓存,命中返回;miss 则查库、写缓存、返回。
写流程:先更新数据库,再删除缓存(删而不是更新缓存)。删除比更新好在哪:
- 更新缓存有写写竞态:两个并发写,后写库的先写缓存,缓存就是旧值
- 删除缓存让下次读重建,天然规避乱序
删缓存的竞态
“先更新库再删缓存”在极端时序下仍会不一致:
读后写回与删缓存交错
概率很低(读要在写删之间完成),但确实存在。解法:
- 延迟双删:删缓存后等一小段时间(主从同步延迟窗口,如 500ms)再删一次。第二次删除兜掉“旧值写回”
- 版本号/时间戳:缓存 value 带版本,写回时版本比缓存旧就丢弃
- 消息队列异步重删:删缓存失败(Redis 抖动)时重试
延迟双删要点
流程:更新库 → 删缓存 → 睡 N 毫秒 → 再删缓存。
- 为什么睡:等主从同步和并发读把旧值写回的动作完成
- N 取多少:主从同步延迟的上界,常规 300 到 500ms;删库要删两次的原因:第一次删可能被旧值写回覆盖
- 局限:睡眠期间请求变慢;极端竞态下仍非绝对一致。属于工程折中
最终一致的可靠方案
| 方案 | 机制 | 特点 |
|---|---|---|
| MQ 异步删 | 写库后发消息,消费端删缓存,失败重试 | 可靠,引入消息中间件 |
| Canal 订阅 binlog | 监听数据库 binlog,变更驱动删缓存 | 与业务解耦,不侵入代码 |
| 重试队列 | 删失败进队列定时重试 | 兜底手段 |
强一致场景的诚实结论:缓存本质是牺牲一致性换性能。读多写少、容忍秒级不一致的业务才适合上缓存;要求强一致的写操作直接写库绕过缓存。
面试追问
- 为什么写时删缓存而不是更新缓存? 更新有写写竞态,删除让读重建,天然规避乱序。删缓存也简单
- 先删缓存再更新库有什么问题? 删完缓存后、写库前,并发读会把旧值写回缓存,不一致窗口更大
- 延迟双删为什么删两次? 第一次删可能被并发读的旧值写回覆盖,等一个主从同步窗口再删一次兜底
- 缓存一致性能做到强一致吗? 不能,目标是最终一致。强一致就绕过缓存
- Canal 方案的优点? 监听 binlog 不侵入业务代码,删缓存失败可重试,可靠性高