缓存和数据库的一致性怎么保证?为什么用删除缓存而不是更新缓存?
简化版
缓存和数据库不一致的根源是更新数据时要操作两个系统,无法天然原子。主流模式是 Cache Aside:读时缓存没有就查库回填;写时先更新数据库,再删除缓存。删除缓存比更新缓存更稳,因为删除是幂等的,能减少并发写导致旧值覆盖新值,也能避免每次写都重新计算复杂缓存。
详细版
Cache Aside 的基本流程:
| 操作 | 流程 |
|---|---|
| 读 | 先读缓存,未命中查数据库,再回填缓存 |
| 写 | 先更新数据库,再删除缓存,下次读重新加载 |
为什么不是先删缓存再更新数据库?因为删缓存后、更新数据库前,如果有读请求进来,会读到数据库旧值并回填缓存,之后数据库更新完成,缓存却长期保存旧值。
为什么删除缓存而不是更新缓存?第一,更新缓存存在并发乱序问题,后完成的旧写可能覆盖先完成的新写。第二,缓存值可能是复杂聚合结果,写时重算成本高,且写多读少时可能白算。删除后按需加载更简单、更稳。
进阶方案包括延迟双删、订阅 binlog 删除缓存、缓存过期时间兜底、重要数据读主库或版本校验。
完整版教学
一、一致性问题的根源
数据库和缓存是两个独立系统。一次写操作如果既要改数据库又要改缓存,就必然是两个步骤。只要是两个步骤,就存在第一步成功、第二步失败,或者两步之间插入并发读写的可能。
所以缓存一致性的目标通常不是绝对强一致,而是缩小不一致窗口,并保证最终能恢复正确。想要绝对强一致,就不能把缓存放在这种异步旁路位置,或者要付出很高的同步协议成本。
二、为什么先更新数据库再删缓存
先删除缓存再更新数据库有明显漏洞:线程 A 删除缓存后还没更新数据库,线程 B 读取发现缓存空,于是查数据库旧值并写回缓存。随后线程 A 更新数据库为新值。结果数据库是新值,缓存是旧值,直到 TTL 到期前都会不一致。
先更新数据库再删除缓存也不是完美,但不一致概率低很多。正常情况下,数据库更新后缓存被删除,下一次读未命中就会读到新值并回填。它是工程上最常用、成本最低、风险较小的方案。
三、为什么删除比更新更稳
更新缓存的问题在并发场景下很隐蔽。两个写请求 A、B 同时修改同一数据,数据库最终以 B 为准。但缓存更新可能因为网络、计算耗时不同,出现 B 先写缓存、A 后写缓存,最终缓存变成 A 的旧值。
删除缓存就没有这个顺序覆盖问题。无论 A 先删还是 B 先删,结果都是缓存不存在。后续读请求查数据库,拿到的是数据库里的最终值。删除操作天然幂等,失败重试也更安全。
四、延迟双删解决什么
先更库再删缓存仍有小概率漏洞:一个读请求先查数据库旧值,写请求随后更新数据库并删除缓存,最后这个读请求把旧值回填缓存。延迟双删是在更新数据库后立即删一次缓存,再延迟几百毫秒删第二次,清理这类并发读回填的旧值。
延迟时间要根据业务读耗时、数据库耗时估算,不是越长越好。它降低概率,不提供绝对保证。对要求更高的场景,需要 binlog 订阅、版本校验或读主库策略。
五、binlog 订阅为什么更可靠
使用 Canal 等组件订阅数据库 binlog,可以把缓存删除从业务代码里解耦出来。只要数据库发生变更,binlog 一定记录,消费端根据变更事件删除对应缓存,失败可以重试。
这种方案适合大型系统统一治理缓存一致性。它的代价是链路更长,引入 Canal/MQ 等组件,也存在短暂延迟。所以通常还会给缓存设置 TTL 作为最终兜底。
六、面试追问与工程边界
面试官常会问“先更库再删缓存能不能保证强一致”。不能。它是工程上概率更低、实现简单的最终一致方案,不是强一致协议。只要数据库和缓存分成两个系统,就存在失败和并发窗口,只能通过重试、TTL、binlog、版本号等方式兜底。
对特别关键的数据,应该少依赖缓存读结果做最终决策。例如下单扣库存时,商品详情缓存可以旧一点,但真正扣减必须回到数据库或强一致库存服务校验。缓存适合加速读,不适合替代权威判断。
七、常见误区与追问
这道题不能只背概念,要把「缓存与数据库一致性」放回真实分布式系统里解释:参与方是谁、状态怎么流转、失败后怎么恢复,以及它在一致性、性能、可用性之间做了什么取舍。
| 回答层次 | 要讲清的内容 | 容易漏掉的边界 |
|---|---|---|
| 核心结论 | 缓存一致性通常用先更新数据库再删除缓存,结合重试、延迟双删、订阅 binlog 和最终一致兜底 | 不要停在名词解释 |
| 流程机制 | 写请求更新数据库 -> 删除缓存 -> 读请求未命中后加载新值 -> 删除失败进入重试或补偿 -> 通过 TTL 和 binlog 最终收敛 | 说明触发方、存储方、确认点和兜底 |
| 工程取舍 | 写库成功后删除缓存失败,旧缓存可能继续存在 5 分钟,直到 TTL 到期或补偿删除 | 缓存提升吞吐但会引入旧值、热点、内存和失效风暴问题 |
缓存与数据库一致性 面试拆解:
1. 写请求更新数据库
2. 删除缓存
3. 读请求未命中后加载新值
4. 删除失败进入重试或补偿
5. 通过 TTL 和 binlog 最终收敛
记忆钩子:先说明缓存承担的读写压力,再拆穿透、击穿、雪崩、热点、一致性和淘汰策略;回答时要紧扣「缓存与数据库一致性」这道题,不要把相邻概念混成一段泛泛的分布式套话。
- 误区:先更新缓存再更新数据库最稳。 两边更新无法原子,缓存可能写入数据库未提交或失败的数据。
- 误区:删除缓存一定不会失败。 网络、权限、Redis 故障都会让删除失败,需要重试补偿。
- 误区:强一致缓存很容易。 缓存和数据库跨系统无法天然原子,多数场景做最终一致。
- 追问:为什么常用删除而不是更新缓存? 删除让下一次读取从数据库加载,避免并发写入旧值。
- 追问:延迟双删解决什么? 降低读写并发下旧值回填缓存的概率。
- 追问:如何增强可靠性? 消息队列重试、binlog 订阅、TTL、缓存版本号和对账。
八、加强记忆
缓存一致性的本质是数据库和缓存两步操作无法原子。主流 Cache Aside 是读未命中查库回填,写先更库再删缓存。删除优于更新,因为删除幂等、避免并发旧值覆盖新值,也能省掉复杂缓存重算。延迟双删降低并发回填旧值概率,binlog 订阅和 TTL 负责最终一致兜底。