Java

双写、延迟双删、订阅 binlog,什么场景下各自更稳?

缓存一致性最怕一上来就争论方案名字。把业务能容忍多久旧值、数据库是不是唯一事实来源、失败后怎么补偿、热点 key 回源有多贵这些前提摆清,再看双写、延迟双删和 binlog/CDC,结论通常会直接收敛。

  • Redis
  • 缓存一致性
  • binlog
  • 延迟双删
  • 系统设计
19 分钟阅读

做缓存一致性设计时,团队最后几乎总会绕到三个方案上:

  • 双写数据库和缓存
  • 更新数据库后做延迟双删
  • 订阅 binlog / CDC,再异步删缓存或刷新缓存

这三个方案几乎每个团队都讨论过,但争论也经常绕成一句没什么帮助的话:

  • 这个方案更优雅
  • 那个方案更实时
  • 这个业界用得更多

真正到线上出问题时,这种抽象比较通常没什么用。

因为缓存一致性真正难的,不是概念上知道这三种方案,而是:

  • 你的业务能容忍多长时间的旧值
  • 数据库是不是唯一事实来源
  • 写入失败和删除失败怎么补偿
  • 热点 key 删除后,数据库能不能扛住回源
  • 并发写入和乱序写入下,会不会把旧值重新种回去

也就是说,双写、延迟双删、binlog 方案之间,不存在脱离场景的“谁绝对更好”,只有“谁在什么条件下更稳”。

我更关心的不是这三个方案哪个名字更高级,而是线上出事时谁更容易补、谁更容易把旧值重新种回去。判断时别先比术语,更该比这几件事:事实来源站在哪边、旧值窗口有多长、失败后有没有补偿、并发乱序会不会把缓存写回旧值、热点回源能不能扛住。

如果你已经看过《缓存一致性问题为什么总在删缓存、改数据库之后出事?》,可以把这篇当成下一步:前一篇把问题链条摊开,这一篇专门拿来做方案取舍。

你现在更卡在哪一步

你现在的目标更值得先看什么为什么
想比较双写、延迟双删、binlog / CDC 哪种更稳直接往下看这篇重点就是把三种常见做法放到同一套取舍里比较
你还没把一致性里的时序、旧值回填和回源放大理顺先看《缓存一致性问题为什么总在删缓存、改数据库之后出事?先把问题链条看清,再谈选型才不会飘
你在处理的是穿透、击穿、雪崩或热 key先去看热点和缓存故障类型相关文章这些问题和一致性选型不是一回事,混着看容易走偏
你要的是某个方案的代码实现细节把这篇当成选型前的判断清单它更偏方案边界和取舍,不替代具体实施文档

把比较口径定清楚

如果你已经在双写、延迟双删和 binlog / CDC 之间摇摆,就直接按本文的比较框架往下看。

这里不想再重复“一致性为什么会出事”,而是把几个真正决定方案优先级的条件摊开:数据库是不是唯一事实来源、业务能容忍多长旧值窗口、失败后怎么补偿、热点回源代价有多高,以及并发乱序是不是常态。

判断顺序可以这样看

如果你现在正在选一致性方案,第一轮别急着比术语,先把数据库是不是唯一事实来源、业务能容忍多短的旧值窗口、写入失败怎么补偿、热点 key 回源代价有多高、以及并发乱序写会不会发生写清。因为双写、延迟双删、binlog / CDC 之间没有脱离场景的优劣,把这些边界写清,后面比较才不会变成空谈。

  1. 先定数据库是不是唯一事实来源。
  2. 再定业务能容忍多短的旧值窗口,而不是先选方案。
  3. 再看写路径分散程度、补偿能力和热点 key 的回源代价。
  4. 如果你更怕两个真相源一起写乱,就不要轻易走双写。
  5. 如果你已经知道自己只能接受最终一致,再在延迟双删和 binlog / CDC 之间比较治理成本和失败模型。

一、别急着讨论方案,先定一个前提:数据库是不是唯一事实来源

这是比较三种方案前最重要的一步。

对大多数业务系统来说,更稳的默认前提通常是:

  • 数据库是唯一事实来源
  • 缓存是派生副本
  • 缓存的任务是加速读,不是跟数据库一起并列做两份真相

一旦这个前提成立,很多方案的优先级就会很不一样。

1. 如果数据库是唯一事实来源

更稳的目标通常是:

  • 先保证数据库写入正确
  • 再让缓存尽快失效或收敛
  • 失败时有补偿链路
  • 接受短时间最终一致,而不是追求假想中的绝对同步

2. 如果你试图让缓存也变成事实来源之一

那你马上就会遇到:

  • 双写顺序
  • 两边失败不一致
  • 并发覆盖和乱序写回
  • 回滚与补偿复杂度爆炸

所以大多数普通业务里,真正稳的方案并不是“把缓存写得像数据库一样可靠”,而是:

  • 承认数据库是主
  • 让缓存做副本
  • 让缓存尽快失效或异步收敛

这也是为什么下面三种方案里,真正值得优先比较的不是“是否实时”,而是“是否更符合数据库为事实来源的前提”。

二、先建立一个比较框架:别先问谁更高级,先问谁在哪些地方更容易翻车

比较这三类方案时,我更建议至少从下面五个维度看:

  1. 旧值窗口有多长
  2. 失败后有没有自然补偿路径
  3. 并发写入和乱序写入时,会不会把旧值写回去
  4. 对热点 key 和回源流量有没有额外副作用
  5. 实现复杂度和维护成本有多高

这五个维度一摆出来,很多原本抽象的争论会马上具体很多。

三、再看双写:看起来最实时,但也最容易把两个真相源一起写乱

双写通常指的是:

  • 写数据库
  • 同步写缓存

有的团队还会进一步细分:

  • 先写数据库再写缓存
  • 或先写缓存再写数据库

但无论顺序怎么排,双写的根问题都一样:

你开始同时维护两个状态源,而且还希望它们时刻一致。

双写的优势

从直觉上看,双写最大的好处是:

  • 缓存更新及时
  • 读请求更少走 miss
  • 对热点 key 看起来比较友好
  • 不像删缓存那样有显式空窗期

双写真正的高风险点

1. 一个成功,一个失败

例如:

  • 数据库写成功,缓存写失败
  • 缓存写成功,数据库回滚或失败

这时你立刻进入补偿地狱。

2. 并发写入乱序覆盖

这是双写最麻烦的一点。

比如:

  1. 请求 A 把值改成 120
  2. 请求 B 紧接着把值改成 130
  3. B 的缓存写先成功
  4. A 的缓存写因为网络或重试晚到一步,又把缓存写回 120

最后就会出现:

  • 数据库是 130
  • 缓存却变回 120

3. 缓存写入逻辑越来越像第二套事务系统

为了修上面这些问题,你很快就会开始加:

  • 版本号
  • 条件更新
  • 幂等控制
  • 重试和补偿
  • 乱序覆盖保护

这时缓存已经不再只是缓存,而像一套简化版数据库同步系统。

双写更适合什么场景

只有在下面这些前提比较明确时,双写才相对更稳:

  • 你非常在意旧值窗口尽量短
  • 写入并发不算夸张,或者能通过版本号防乱序
  • 缓存写入支持条件更新或至少带版本校验
  • 团队能长期维护这套写入一致性复杂度

如果这些前提做不到,双写通常不是“更先进”,而是“更脆弱”。

四、再看延迟双删:它不追求绝对同步,而是用两次删除覆盖并发窗口

延迟双删的典型思路是:

  1. 更新数据库
  2. 删除缓存
  3. 过一小段时间再删一次缓存

也有团队会写成“先删缓存、再写数据库、延迟再删”,但对大多数数据库为事实来源的业务来说,更常见的稳妥主线还是:

  • 先更新数据库
  • 尽量在事务提交后删缓存
  • 再做一次延迟补删

延迟双删想解决什么

它真正要解决的是:

  • 第一次删缓存之后
  • 可能有并发读请求去数据库读到旧值
  • 又把旧值回填回缓存

于是隔一段时间再删一次,试图把这部分并发窗口盖住。

延迟双删的优势

  • 保持数据库为事实来源
  • 实现难度比双写低
  • 对大多数可容忍短暂旧值的业务更友好
  • 不强迫缓存承担第二真相源角色

延迟双删的局限

1. 它本质上是概率补丁,不是确定性方案

因为你永远无法保证:

  • 第二次删除恰好踩中所有竞争窗口
  • 事务时间、网络延迟、下游耗时都刚好在你的估计范围内

2. 延迟多久没有标准答案

  • 太短,覆盖不了真实窗口
  • 太长,旧值窗口又太长

3. 删除失败仍然需要补偿

如果第二次删也失败,而且没有重试、告警和补偿链路,那还是会留下旧值。

4. 热点 key 删除后可能带来回源抖动

尤其是热点对象,一删缓存就会出现:

  • miss 增多
  • 回源数据库抬高
  • 连接池和接口 RT 被一起放大

所以延迟双删从来不是只看一致性,还要看数据库和回源承压能力。

延迟双删更适合什么场景

更适合:

  • 数据库明确是事实来源
  • 业务能容忍短时间旧值
  • 读多写少
  • 能接受最终一致
  • 已经有基本的删除重试和补偿能力

对这类场景来说,延迟双删通常比双写更稳,也更容易维护。

五、再看 binlog / CDC:它更像“数据库提交后驱动缓存收敛”的异步一致性链路

binlog / CDC 方案的核心思路是:

  • 应用只负责把数据库写对
  • 数据库提交后,由 binlog 或 CDC 事件驱动缓存删除或刷新

它的最大优点在于:

  • 删除动作天然站在“数据库已经成功提交”之后
  • 不需要业务代码在事务里自己判断那么多时序细节

binlog 方案的优势

1. 更符合数据库为事实来源

因为它的触发点是:

  • 数据库提交成功
  • 数据变化成为事实之后
  • 再驱动缓存收敛

2. 对业务代码侵入小

业务侧不一定需要在每条写路径里手动维护复杂删除逻辑。

3. 更适合多写路径、多服务、多语言系统

如果同一份数据可能被:

  • Java 服务改
  • Python 脚本改
  • 后台运营系统改
  • 批任务改

那把缓存失效只写在某一层业务代码里,天然容易漏。binlog 方案在这里会更统一。

binlog 方案的局限

1. 它不是强一致,而是更可控的异步最终一致

因为从数据库提交到缓存失效,仍然存在传播延迟。

2. 消费链路本身也会失败

例如:

  • 消费积压
  • 消息延迟
  • 订阅程序挂掉
  • 映射规则写错
  • 下游 Redis 删除失败

如果这条链没人监控、没人补偿,问题只是从应用代码里搬到了 CDC 流水线里。

3. 变更映射复杂时,维护成本不低

最难的往往不是“收到 binlog”,而是:

  • 这条表变更对应哪些缓存 key
  • 需要删哪些聚合视图
  • 是否跨表、跨对象、跨服务

binlog 方案更适合什么场景

更适合:

  • 数据库是明确主事实源
  • 写路径分散,不想每条业务代码都维护失效逻辑
  • 可以接受短时间最终一致
  • 有能力建设事件消费、监控、重试和补偿体系

如果团队基础设施比较成熟,binlog / CDC 通常会比在业务代码里到处散落删除逻辑更稳。

六、把三种方案放到同一张表里理解:它们各自“稳”的条件不一样

如果用一句话分别概括:

  • 双写:更追求实时,但最怕并发乱序和双边失败
  • 延迟双删:更偏工程折中,前提是能接受短暂旧值和热点回源
  • binlog / CDC:更偏平台化收敛,前提是你能维护好异步消费链路

换成更实战的判断:

1. 如果你的业务最怕旧值窗口,但写并发不高、可带版本控制

双写才有讨论空间。

2. 如果你的业务默认接受最终一致,数据库就是主,写路径不算特别分散

延迟双删通常是更稳的默认起点。

3. 如果你的写路径很多、改数据的人很多、服务很多,希望统一收口

binlog / CDC 往往更适合做中长期方案。

七、别漏掉一个关键维度:热点 key 和回源代价,会改变三种方案的“稳不稳”

缓存一致性方案比较时,很多文章只比时序,不比回源副作用,这是不够的。

1. 双写对热点 key 的好处

它减少了主动删缓存造成的 miss 空窗期。

2. 延迟双删和 binlog 删除,对热点 key 更容易引发回源

因为它们的默认动作通常是:

  • 删缓存
  • 让后续读请求再回源重建

如果这个 key 很热:

  • 一次删除就可能带来大批 miss
  • 数据库和连接池很容易抖

所以你在比较方案时,一定要同时问:

这个 key 热不热?删掉后数据库能不能扛住回源?

如果这个问题不先回答,很多一致性方案在纸面上很漂亮,线上却很脆。

八、一个更实用的选择顺序:先定业务容忍度,再选方案,不要反过来

如果以后再碰到“到底双写、延迟双删还是 binlog 更稳”,我更建议按这个顺序选。

第 1 步:先定业务容忍度

先回答:

  • 这个字段能不能接受短暂旧值
  • 能接受多长
  • 是展示型字段,还是交易决策字段

第 2 步:再定事实来源

通常更稳妥的默认前提是:

  • 数据库是唯一事实来源

第 3 步:再看写路径分散程度

  • 写路径集中,还是很多系统都在改
  • 是否适合在业务代码里收口
  • 是否更适合走 CDC 统一收口

第 4 步:再看热点和回源代价

  • key 热不热
  • 删除后数据库扛不扛得住
  • 是否需要互斥回源、异步刷新或其他保护手段

第 5 步:最后才选实现方案

通常会收敛成:

  • 低并发、短旧值敏感、可带版本控制:才考虑双写
  • 一般业务、可最终一致:优先延迟双删或提交后删缓存
  • 写路径多、需要统一治理:考虑 binlog / CDC

九、一个典型案例:为什么同样是价格字段,有团队选延迟双删,有团队选 binlog

假设两个团队都在处理商品价格缓存一致性。

场景 A:单服务维护、写路径集中

特点:

  • 只有一个核心 Java 服务写价格
  • 更新频率中等
  • 业务能容忍几百毫秒旧值
  • 团队暂时没有成熟 CDC 基础设施

这类场景下,更合适的做法通常是:

  • 数据库提交后删缓存
  • 再做延迟补删
  • 补齐删除失败重试和告警

场景 B:价格会被多个系统更新

特点:

  • 运营后台会改
  • 批量调价脚本会改
  • 促销系统会改
  • 不同语言服务都可能改

这时如果还把失效逻辑散在每条业务代码里,非常容易漏。

更合适的方向通常就是:

  • 让数据库写入成为唯一入口事实
  • 用 binlog / CDC 驱动价格缓存失效或刷新

这个例子很能说明:不是某个方案天然更高级,而是它更适合某种系统边界。

十、关键误判:这类方案比较最容易在哪些地方走偏

误判 1:双写更实时,所以一定更好

错。

它只是旧值窗口更短,但并发乱序和失败补偿复杂度通常更高。

误判 2:延迟双删是经典方案,所以可以无脑套

错。

如果热点 key 很热、数据库回源代价很高,删缓存本身就可能成为放大器。

误判 3:binlog 方案最优雅,所以一定最稳

也错。

如果 CDC 消费链路没有监控、重试、积压治理,它同样会悄悄失效。

误判 4:只比较一致性,不比较回源代价

很多线上事故不是旧值先出事,而是删缓存后的回源先把数据库打疼。

误判 5:先选方案,再去硬套业务容忍度

顺序反了。

更可靠的做法永远是:先定容忍度和边界,再选方案。

十一、FAQ:双写、延迟双删、binlog 方案里最常被问到的几个问题

1. 默认应该优先考虑哪个方案?

对大多数“数据库是事实来源、业务可接受最终一致”的普通业务,默认更推荐:

  • 数据库提交后删缓存
  • 必要时再加延迟补删或异步补偿

2. 什么时候双写才值得考虑?

当你非常在意旧值窗口,而且能处理版本控制、幂等、乱序覆盖和双边失败时,双写才有讨论价值。

3. binlog / CDC 是不是比延迟双删更高级?

不该这么比。

它更适合写路径分散、需要统一收口的系统,但前提是你有能力维护这条异步消费链路。

4. 延迟双删是不是银弹?

不是。

它只是用第二次删除覆盖一部分并发窗口,解决不了所有乱序、失败和热点回源问题。

十二、把方案比较落回现场

下面这些文章分别补不同问题,不是把双写、延迟双删、binlog / CDC 再换个标题重讲一遍。

如果你还在补缓存侧证据

如果影响已经扩到别的链路

如果你准备按现场一步步拆

  1. 先看《缓存一致性问题为什么总在删缓存、改数据库之后出事?》,把删除失败、旧值回填和回源放大这条主线先吃透。
  2. 回到这篇,再把双写、延迟双删、binlog / CDC 放进同一套比较框架里看代价和收益。
  3. 如果你大概率会走删缓存路线,接着看 缓存 TTL 设计不当,为什么会把高峰流量集中打回数据库?缓存回源流量突然放大时,到底先查业务热点、命中率还是数据库压力?
  4. 如果回源已经把数据库和接口一起拖慢,再去看 缓存层出问题后,为什么应用和数据库会一起变慢的等待型链路?
  5. 如果你已经看到事务、连接池和接口 RT 一起被放大,再把数据库等待链相关文章连起来看。

十三、最后总结:三种方案没有绝对优劣,只有谁更符合你的事实来源和失败模型

缓存一致性方案比较里,最容易把人带偏的地方就是:

  • 总想找一个放之四海而皆准的“最优方案”
  • 却没有先定清楚业务容忍度、事实来源和失败补偿边界

真正更值得遵守的主线应该是:

先承认数据库是不是唯一事实来源,再看业务能容忍多短的旧值窗口、写路径有多分散、热点 key 的回源代价有多高,最后再选双写、延迟双删还是 binlog / CDC。

只要这条顺序不乱,很多原本停留在术语层的争论,最后都会收敛成一个更具体、更可落地的工程决策问题。