缓存更新如何保证一致性

wen 网络安全 2

缓存更新的“鬼故事”:如何用“读写穿透”与“延迟双删”终结数据不一致?

目录导读

  1. 核心矛盾:为什么缓存和数据库总会“打架”?
  2. 常见陷阱:先删缓存还是先更新库?——这些“死法”你得知道
  3. Cache-Aside模式 + 延迟双删(最常用,但有坑)
  4. 读写穿透(Read/Write Through)(架构升级,适合高并发)
  5. 异步队列 + 最终一致性(终极武器,但别滥用)
  6. 高频问答:面试官最爱问的三个缓存一致性难题
  7. 实战总结:不同场景下该选哪个方案?

核心矛盾:为什么缓存和数据库总会“打架”?

举个例子:用户A修改了自己的昵称,数据库里已经更新成了“新昵称”,但缓存里还留着旧的“老昵称”,此时用户B读取这个昵称,就会看到错误信息。本质问题是:对同一份数据,缓存和数据库是两个独立的存储系统,它们的更新操作无法原子化。

缓存更新如何保证一致性

如果不做任何处理,数据一致性被破坏的后果在电商、社交、金融等场景中是不可接受的(比如看到别人账户余额没变)。

常见陷阱:先删缓存还是先更新库?

先写数据库再删缓存:

  • 场景:更新数据库成功,但删除缓存失败,下次读请求来时,缓存依然是旧数据。
  • 后果:缓存永久性污染,除非缓存过期,否则数据一直错。

先删缓存再写数据库:

  • 场景:线程A删了缓存,但还没写完数据库,此时线程B来读,发现缓存没有,就去数据库读到了旧数据,然后回填到缓存。
  • 后果:缓存里被填入了旧数据,线程A的数据库更新白费了。

单纯依靠操作顺序解决不了问题,需要引入额外机制。

Cache-Aside模式 + 延迟双删

核心逻辑

  1. 首次读:缓存没有 → 读数据库 → 写入缓存。
  2. 写操作时:
    • 先删缓存。
    • 再写数据库。
    • 延迟一段时间(比如500ms),再次删除缓存。

为什么要“延迟双删”?因为在“删→写→删”之间的空隙里,其他线程可能已经读到了旧数据并写入了缓存,第二次删除就是清除这个“漏网之鱼”。

优缺点

  • 优点:实现简单,对现有代码侵入小。
  • 缺点:
    • 延迟时间难以精准控制(太短清不掉并发读,太长影响性能)。
    • 第二次删除可能失败(可以配合消息队列重试)。

适用场景:读写比例较高、对短暂不一致容忍度较高的业务(如用户个人资料更新)。

读写穿透(Read/Write Through)

核心逻辑

把缓存层变成一个“代理”,应用程序不直接操作数据库,而是通过缓存层统一处理:

  • 读穿透:缓存没有 → 缓存层从数据库加载数据并返回。
  • 写穿透:写请求先发到缓存层 → 缓存层自己更新数据库 → 同时更新本地缓存。

这种模式下,读和写都由缓存层管理,天然避免了并发乱序问题。

优缺点

  • 优点:一致性高,代码简化(业务层只关心缓存)。
  • 缺点:
    • 缓存层变成单点或复杂中间件(如Redis + AOF持久化需要谨慎)。
    • 对写入性能有影响(因为要通过缓存层同步写数据库)。

适用场景:对一致性要求极高、且写入频率不高的核心系统(如支付、订单状态)。

异步队列 + 最终一致性

核心逻辑

不用追求“强一致”,而是接受短时间不一致,靠异步消息保证最终一致:

  1. 数据库写成功后,发一条消息到队列(如Kafka)。
  2. 消费者接收到消息后,再删除或更新缓存。
  3. 如果删除失败,通过死信队列重试(直到成功)。

关键在于:消息队列解决了“操作原子性”问题(要么DB和MQ都成功,要么都失败)。

优缺点

  • 优点:解耦合,能处理大量并发写,能保证最终一致性。
  • 缺点:
    • 存在短暂不一致窗口(取决于MQ消费延迟)。
    • 业务复杂度增加(需要引入MQ、死信队列等组件)。

**适用场景:高并发写、对短暂不一

抱歉,评论功能暂时关闭!