双写一致性问题

背景

缓存 + 数据库 的经典架构里,缓存通常作为加速层来缓解数据库压力。
但在更新数据时,如果操作顺序处理不当,就可能导致缓存与数据库之间出现短暂的不一致。

常见的更新顺序有两种:

  1. 先写数据库,再写缓存

    • 优点:简单直观,能保证数据库最终一致性。
    • 缺点:在删除缓存和数据库更新之间的时间差,可能有请求先查到老数据,一致性弱。
  2. 先写缓存,再写数据库

    • 优点:能一定程度上减少脏读。
    • 缺点:如果数据库更新失败,就会丢掉缓存,反而更糟。

因此我们大多时候会选择先写数据库再写缓存。

删除与更新

在写缓存的时候我们可以选择删除或者更新,大多数时候我们会选择更新,因为[1]:

删除缓存 (Cache-Aside) 更新缓存
复杂程度 简单 复杂
幂等性 天然幂等(无论怎么删,最终结果都是缓存被删除了) 非幂等操作
并发写安全 安全 (删除顺序不影响最终状态) 不安全 (并发更新可能导致缓存数据错乱或覆盖,即使是redis也只保证单个命令是安全的)
效率 (直接删除,不关心数据) (可能频繁更新一个后续无人读取的值,消耗CPU和带宽,特别是当你在维护一个复杂的缓存时)
数据最终一致性 更易保证 (删除不依赖数据) 更难保证 (依赖数据,若顺序错误可能脏甚至是错误)

缓存失效策略(Cache-Aside / Lazy-Loading)

写数据库,删除缓存,下次请求重建缓存,简单可靠。

双删

双删也是经常提到的办法,属于一种增强版的写数据库后删除缓存,能够减少不一致窗口期。

在更新数据库之后,执行两次缓存删除操作

事务A 事务B
更新数据库
删除缓存(第一次)
访问数据
缓存未命中,读取数据库
返回数据库中的数据
延时一段时间后删除缓存(第二次)

第二次删除缓存通常会延迟,目的是解决以下问题:

事务A 事务B
写数据库 val = 37
写数据库 val = 42
写缓存 val = 42
写缓存 val = 37
  1. 延迟时间的选择
    延迟需要大于业务中可能的并发请求处理时间。
  2. 幂等性
    删除缓存操作需要是幂等的,即多删几次不会有副作用。

双删缺陷

弱一致性

  • 在第一次删除和第二次删除之间,如果有读请求访问数据,还是可能会从数据库加载旧数据回填缓存 → 读到脏数据
  • 因此双删保证的只是 最终一致性无法保证请求级别的强一致性

令人纠结的时间

  • 延迟太短:第二次删除可能还没覆盖到并发读回填的缓存,最终一致性可能都无法保证(长时间或永久脏缓存)。
  • 延迟太长:第二次删除的间隔内,缓存可能被大量请求读写回填,脏数据存在的时间过长(脏数据多)。

实际上,延迟双删更多是工程妥协:在读多写少、对短时间不一致可容忍的业务场景下适用(个人感觉有点骚操作,不过能保证最终一致性的同时还能减少脏数据时间总是好的)。

脚注

[1] 删除是 KISS原则YAGNI原则 的体现。此外,好的设计往往是对需求的精准把握,我们作为设计师要从需求好好考虑,找到最关键的问题,简单并不是简陋。