深蓝黑背景品牌封面:左侧大标题「写了数据库,缓存却没更新」,副标题「缓存与数据库的双写一致性」;右侧原创装饰——青色数据库圆柱与紫色缓存方块之间两条同步箭头,其中一条画着红色叉号,表示同步失败;顶部标注「经典面试题解析 · 缓存」与「Day 02 · 09-20」 图 1:本文封面,点明题目与解题方向。

先说明属性:这是一篇经典题解析,题目出自本地历史面试资料(见文末来源说明),与任何公司的当前招聘无关。缓存与数据库的一致性问题没有「一招鲜」,考的是你能不能把每种方案的失败场景推演出来。

想象面试现场,面试官问:「你的服务用 Redis 做缓存。更新数据的时候,数据库改成功了,缓存没改成,用户读到旧数据,怎么办?」

第一步,审题。

先明确一个前提:数据库和缓存是两个独立系统,之间没有分布式事务,任何「两步都成功」的承诺都给不了。所以这道题的正确打开方式不是追求绝对一致,而是讲清楚三件事:主流方案选哪个、它还会在哪里失败、失败后怎么补救。

第二步,基础回答:为什么是「先写库,再删缓存」。

先把一个反直觉的选择讲清楚:更新数据时,对缓存的操作为什么是「删除」而不是「更新」。两个理由。其一,缓存值可能是多次计算拼出来的,与其每次写库都重算一遍(可能根本没人读),不如删掉、等下次读的时候再懒加载。其二,两个并发写各自更新缓存,到达顺序无法保证,缓存里可能留下较旧的那次计算结果;删除操作则没有这个问题。

顺序上,主流方案是先更新数据库、再删除缓存。反过来「先删缓存再写库」有一个经典失败窗口:线程 A 删掉缓存后还没写库,线程 B 来读,缓存未命中,读到旧值并回填进缓存;A 写完库之后,缓存里躺着的已经是旧数据了。

先写库再删缓存也不是完美:理论上存在一个极小窗口——缓存刚好过期、读线程回填旧值的瞬间与写线程交错。但这个窗口要求「读比写还慢」,实际中极少发生,因此它是工程上的标准答案。

第三步,关键追问:删除失败了怎么办。

这才是题目真正的落点。删缓存那一步可能因为网络抖动、Redis 重启而失败,缓存里留下脏数据。补救分三层。

第一层,重试。删除失败立即重试几次,覆盖瞬时故障。重试也失败,就把删除任务投递到消息队列里异步重试,直到成功——把「必须删」变成「最终一定删」。

第二层,订阅变更日志。更彻底的做法是让缓存更新与业务代码解耦:MySQL 官方文档说明,二进制日志(binlog)记录了所有对表数据的变更事件,本身就是给复制链路用的。用 Canal 这类组件订阅 binlog,数据库一变更就触发缓存删除,业务代码只管写库,删缓存的失败率与代码侵入都随之下降。

第三层,过期时间兜底。无论用哪一层,所有缓存键都必须设 TTL。TTL 是最后一道保险:即使前面所有机制都失效,脏数据也会在过期后被自然清除,不一致被限制在有限的时间窗内。

方案对比图:方案 A「先写库再更新缓存」——两个写线程交错时缓存被旧值覆盖;方案 B「先删缓存再写库」——删除后读线程把旧库值回填缓存,脏数据长期存在;方案 C「先写库再删缓存」是常用基线,只剩「删除失败」的小概率窗口,配合重试可控。底部三个增强手段:延迟双删(写完隔一小段时间再删一次)、删除进消息队列自动重试、订阅数据库 binlog 异步同步缓存 图 2:三种双写方案的失败场景对比,以及三个增强手段。

第四步,可能的追问。

「能不能做到强一致?」可以,代价是分布式锁或队列串行化所有读写,性能基本退回没有缓存的水平——所以工程上普遍接受「最终一致 + 缩短窗口」。「读多写极少还要缓存吗?」要,但一致性问题几乎不出现,方案可以更简单。「延时双删(写库前后各删一次)靠谱吗?」它能压缩先写库方案的残留窗口,但延迟时间靠拍脑袋,只能作为补充手段,不能替代重试与 TTL。

工程应用与 AI 关联。

AI 应用几乎标配缓存:会话上下文、embedding、推理结果都放在 Redis 里。缓存与存储不一致在 AI 场景的表现更刺眼——用户改了知识库文档,问答机器人还在引用旧内容答非所问。双写一致性是缓存架构的第一课,也是 AI 应用上线前必须画清楚的时序图。

易错点清单。

一,把「更新缓存」当默认动作,忽视懒加载与并发写乱序。二,只讲正常流程,讲不出每种方案的失败窗口。三,删除失败只靠 try-catch 打印日志,没有重试和队列补偿。四,缓存键不设 TTL,把脏数据的存活期交给运气。

回到面试现场,建议的答题节奏:先给出标准方案与「删而非更」的理由,再主动推演它的残留窗口,然后按「重试 → 消息队列 → binlog 订阅 → TTL 兜底」的层次讲补偿,最后点明最终一致是工程共识。方案的缺点讲得越主动,回答越可信。

如果你的业务要求「用户改完设置后下一次读取必须是新值」,上面哪一层机制能给出这个保证,代价又是什么?欢迎讨论。

参考资料

  • The Binary Log(MySQL 8.4 官方文档:binlog 记录表数据变更事件、用于复制与时间点恢复、事务提交前写入) — https://dev.mysql.com/doc/refman/8.4/en/binary-log.html