缓存更新的“鬼故事”:如何用“读写穿透”与“延迟双删”终结数据不一致?
目录导读
- 核心矛盾:为什么缓存和数据库总会“打架”?
- 常见陷阱:先删缓存还是先更新库?——这些“死法”你得知道
- Cache-Aside模式 + 延迟双删(最常用,但有坑)
- 读写穿透(Read/Write Through)(架构升级,适合高并发)
- 异步队列 + 最终一致性(终极武器,但别滥用)
- 高频问答:面试官最爱问的三个缓存一致性难题
- 实战总结:不同场景下该选哪个方案?
核心矛盾:为什么缓存和数据库总会“打架”?
举个例子:用户A修改了自己的昵称,数据库里已经更新成了“新昵称”,但缓存里还留着旧的“老昵称”,此时用户B读取这个昵称,就会看到错误信息。本质问题是:对同一份数据,缓存和数据库是两个独立的存储系统,它们的更新操作无法原子化。

如果不做任何处理,数据一致性被破坏的后果在电商、社交、金融等场景中是不可接受的(比如看到别人账户余额没变)。
常见陷阱:先删缓存还是先更新库?
先写数据库再删缓存:
- 场景:更新数据库成功,但删除缓存失败,下次读请求来时,缓存依然是旧数据。
- 后果:缓存永久性污染,除非缓存过期,否则数据一直错。
先删缓存再写数据库:
- 场景:线程A删了缓存,但还没写完数据库,此时线程B来读,发现缓存没有,就去数据库读到了旧数据,然后回填到缓存。
- 后果:缓存里被填入了旧数据,线程A的数据库更新白费了。
单纯依靠操作顺序解决不了问题,需要引入额外机制。
Cache-Aside模式 + 延迟双删
核心逻辑
- 首次读:缓存没有 → 读数据库 → 写入缓存。
- 写操作时:
- 先删缓存。
- 再写数据库。
- 延迟一段时间(比如500ms),再次删除缓存。
为什么要“延迟双删”?因为在“删→写→删”之间的空隙里,其他线程可能已经读到了旧数据并写入了缓存,第二次删除就是清除这个“漏网之鱼”。
优缺点
- 优点:实现简单,对现有代码侵入小。
- 缺点:
- 延迟时间难以精准控制(太短清不掉并发读,太长影响性能)。
- 第二次删除可能失败(可以配合消息队列重试)。
适用场景:读写比例较高、对短暂不一致容忍度较高的业务(如用户个人资料更新)。
读写穿透(Read/Write Through)
核心逻辑
把缓存层变成一个“代理”,应用程序不直接操作数据库,而是通过缓存层统一处理:
- 读穿透:缓存没有 → 缓存层从数据库加载数据并返回。
- 写穿透:写请求先发到缓存层 → 缓存层自己更新数据库 → 同时更新本地缓存。
这种模式下,读和写都由缓存层管理,天然避免了并发乱序问题。
优缺点
- 优点:一致性高,代码简化(业务层只关心缓存)。
- 缺点:
- 缓存层变成单点或复杂中间件(如Redis + AOF持久化需要谨慎)。
- 对写入性能有影响(因为要通过缓存层同步写数据库)。
适用场景:对一致性要求极高、且写入频率不高的核心系统(如支付、订单状态)。
异步队列 + 最终一致性
核心逻辑
不用追求“强一致”,而是接受短时间不一致,靠异步消息保证最终一致:
- 数据库写成功后,发一条消息到队列(如Kafka)。
- 消费者接收到消息后,再删除或更新缓存。
- 如果删除失败,通过死信队列重试(直到成功)。
关键在于:消息队列解决了“操作原子性”问题(要么DB和MQ都成功,要么都失败)。
优缺点
- 优点:解耦合,能处理大量并发写,能保证最终一致性。
- 缺点:
- 存在短暂不一致窗口(取决于MQ消费延迟)。
- 业务复杂度增加(需要引入MQ、死信队列等组件)。
**适用场景:高并发写、对短暂不一