Skip to content

缓存一致性

Cache Aside 模式、更新与删除顺序深度推导、延迟双删、Canal 监听 binlog 工业级最终一致性方案。

Updated View as Markdown
For humans

缓存一致性

校招考察频度:★ 高频(核心必背·高频生死线) 一句话速记:旁路缓存走标配(先更数据库,后删缓存);为什么不先删缓存?读写并发必写回脏数据;为什么是删而不是改?防无谓计算与并发写写乱序;兜底靠 Canal 监听 binlog 异步重试删缓存。

缓存与数据库双写一致性是后端工程与系统设计面试中被盘问得最细的硬骨头之一。考官不仅看你背不背得出结论,更看你能不能用严格的并发时序推导出每种方案的缺陷与边界。


一、为什么是“删除缓存”而不是“更新缓存”?

面试官常设的初级陷阱:“每次更新数据库后,顺便执行 redis.set(key, newVal) 不好吗?为什么要删缓存?”

两大不可接受的缺陷:

  1. 写写并发乱序(数据脏掉):
    假设有两个并发写请求 A 和 B:
    • A 更新数据库为 1;
    • B 更新数据库为 2;
    • 由于网络抖动,B 的缓存更新先到达 Redis,写入 2;
    • A 的缓存更新后到达 Redis,写入 1!
    • 结果:数据库是 2(B 的结果),缓存是 1(A 的结果),两者永久不一致!
  2. 严重的计算资源浪费(Lazy 懒加载哲学):
    很多缓存值不是从单张表查出来直接能用的,而是经过复杂连表查询、序列化拼接计算得出的。如果每次写入都更新缓存,但期间根本没有人来读这个 Key,这等于白白浪费了昂贵的 CPU 与内存!用“删除”代替“更新”,让下一次真正的读请求触发懒加载计算,才是最高效的做法。

二、为什么是“先更新 DB,后删除缓存”,而不是“先删缓存,后更新 DB”?

这是大厂面试最经典的推导演练题:

1. 先删缓存,后更新数据库(并发读写必死!)

严重并发碰撞时序:
1. 请求 A(写请求):先执行了 redis.del("user_1");
2. 恰好此时,请求 B(读请求)进来了:
   -> 查缓存,发现 miss;
   -> 查数据库,读出了旧数据(如 age = 18);
3. 此时请求 A 才慢吞吞地更新了数据库(将 age 改成了 20);
4. 请求 B 将刚才读出来的旧数据 age = 18 写回了 Redis 缓存!
结果:数据库是 20,缓存被请求 B 刷新成了 18!如果不等 TTL 自然过期,缓存将一直返回脏数据!
发生概率:极高!因为写数据库往往涉及磁盘 I/O 和事务提交,耗时几十毫秒;而读操作极快(几毫秒),读请求极易插在写操作中间。

2. 先更新数据库,后删除缓存(Cache Aside 推荐标配)

理论碰撞时序:
1. 缓存刚好失效(或者刚好被驱逐);
2. 请求 A(读请求):查缓存 miss,查数据库得到旧值 v1;
3. 紧接着请求 B(写请求):更新数据库为 v2,并删除了缓存;
4. 请求 A 将旧值 v1 写回缓存。
发生概率:极低!要发生上述不一致,必须满足“步骤 2 查库”比“步骤 3 写库+删缓存”还要晚完成。但实际上“读内存/读库”的速度远快于“写库事务提交+网络删缓存”,因此几乎不可能发生这种倒挂。

“先更新库再删缓存”在极端时序下仍会不一致:

读后写回与删缓存交错

概率很低(读要在写删之间完成),但确实存在。解法:

  • 延迟双删:删缓存后等一小段时间(主从同步延迟窗口,如 500ms)再删一次。第二次删除兜掉”旧值写回”
  • 版本号/时间戳:缓存 value 带版本,写回时版本比缓存旧就丢弃
  • 消息队列异步重删:删缓存失败(Redis 抖动)时重试

延迟双删要点

流程:更新库 → 删缓存 → 睡 N 毫秒 → 再删缓存。

  • 为什么睡:等主从同步和并发读把旧值写回的动作完成
  • N 取多少:主从同步延迟的上界,常规 300 到 500ms;删库要删两次的原因:第一次删可能被旧值写回覆盖
  • 局限:睡眠期间请求变慢;极端竞态下仍非绝对一致。属于工程折中

最终一致的可靠方案

方案机制特点
MQ 异步删写库后发消息,消费端删缓存,失败重试可靠,引入消息中间件
Canal 订阅 binlog监听数据库 binlog,变更驱动删缓存与业务解耦,不侵入代码
重试队列删失败进队列定时重试兜底手段

强一致场景的诚实结论:缓存本质是牺牲一致性换性能。读多写少、容忍秒级不一致的业务才适合上缓存;要求强一致的写操作直接写库绕过缓存。

面试追问

  1. 为什么写时删缓存而不是更新缓存? 更新有写写竞态,删除让读重建,天然规避乱序。删缓存也简单
  2. 先删缓存再更新库有什么问题? 删完缓存后、写库前,并发读会把旧值写回缓存,不一致窗口更大
  3. 延迟双删为什么删两次? 第一次删可能被并发读的旧值写回覆盖,等一个主从同步窗口再删一次兜底
  4. 缓存一致性能做到强一致吗? 不能,目标是最终一致。强一致就绕过缓存
  5. Canal 方案的优点? 监听 binlog 不侵入业务代码,删缓存失败可重试,可靠性高
Navigation

Type to search…

↑↓ navigate↵ selectEsc close