🗄️再见,延迟双删:我们在真实生产环境中如何做缓存一致性
技术分享|2025-8-6|最后更新: 2025-9-26
type
status
date
slug
summary
tags
category
icon
password
Status
提到缓存,一个绕不开的话PEG问题就是缓存与数据库的一致性
在学习缓存理论时,我对“延迟双删”这个精巧的设计印象深刻。它通过“删除缓存 -> 更新数据库 -> 延迟再次删除缓存”这三步,似乎完美地解决了“脏数据”问题。
然而,一个残酷的现实是:在我分析过的大量真实、高并发的生产代码中,这个理论上的“优等生”却鲜有出场机会。
为什么?在真实业务的严苛要求下,我们究竟是如何保证缓存一致性的?这篇文章将通过一个我们真实的广告竞价(ADX)系统案例,带你深入了解那些在生产环境中真正被信赖和广泛使用的缓存策略。

案例研究:高性能广告竞价系统(ADX)的缓存设计

广告竞价是一个对性能和时效性要求到极致的场景。一次竞价请求必须在几十毫秒内完成,数据每时每刻都在高频变化。让我们看看这样的系统是如何设计缓存的。

核心策略一:Cache Aside + 读后即删

该系统的核心出价缓存(BidCache)并未使用复杂的更新策略,而是采用了极其简洁高效的 Cache Aside Pattern,并附带了一个特殊操作:读后即删
  1. 读操作:应用先从 Redis 缓存中读取出价数据。
  1. 缓存命中:如果命中,立即从缓存中删除该条目,然后返回数据。
  1. 缓存未命中:查询后端数据库或服务,获取出价数据后返回给应用(通常不回写到缓存,因为一次竞价的响应是唯一的)。
Go
为什么这么设计?
  • 业务特点决定:广告出价响应是一次性的,用完即作废,读后即删完美契合。
  • 性能优先:没有任何多余的写操作或延迟等待,最大化读写性能。
  • 天然一致:数据用完就删,不存在“脏数据”污染后续请求的可能。

核心策略二:多层缓存架构 + TTL 自动过期

除了核心的出价缓存,系统还广泛使用了分层和TTL机制:
  • 多层缓存:使用多个专用的 Redis 分布式缓存实例(主缓存、用户标签、频次控制、算法模型等),并配合进程内的 fastcache + sync.Map 作为热点数据的L1缓存。
  • TTL 自动过期:几乎所有的缓存都设置了较短的TTL(从秒级到分钟级)。依赖 Redis 的自动过期机制来清理数据,这是保证最终一致性、防止垃圾数据堆积的最简单可靠的方式。
在这个场景下,“延迟双删”不仅毫无用武之地,反而会因为引入不必要的延迟和复杂度而成为性能瓶颈。
 

延迟双删的“不舒适区”:为什么我们在生产中很少用它?

通过上面的案例,我们不难发现,好的架构总是与业务场景深度绑定。延迟双删作为一个“理论完美”的方案,在现实中却面临着诸多挑战。

1. 复杂度与收益不匹配

为了解决一个在绝大多数场景下极小概率发生的“读写并发”问题,引入一个需要额外维护延迟任务的复杂机制,这在工程上往往是得不偿失的。

2. “延迟多久”是个玄学问题

延迟500毫秒?1秒?这个时间需要大于数据库主从同步的延迟。但在企业级别复杂的分布式环境中,网络抖动、数据库负载都可能导致这个延迟时间变得不可预测。依赖一个不确定的“魔法数字”来保证确定性,是架构设计的大忌。
 

3. 更好的替代方案层出不穷

工程的本质是权衡(Trade-off)。现实中,我们有更多、更简单、更可靠的武器库来应对不同场景的一致性需求。

生产环境的设计:真实、可靠的一致性策略

下面,让我们看看在电商、金融、内容等核心业务中,那些经受了真实流量考验的缓存一致性策略。

策略一:简单删除 + TTL过期(90%场景的首选)

这是最常见、最简单的策略,也被称为 Cache Aside (Write-Invalidate)
  • 流程:先更新数据库,再直接删除缓存。
  • 优点:简单、高效、可靠。
  • 缺点:理论上,在极端的并发情况下(更新DB后,删除缓存前,有另一个读请求穿透到DB读了旧数据并写回缓存),可能导致短暂数据不一致。
  • 适用场景:绝大多数能容忍秒级数据不一致的场景。比如用户信息、商品介绍、文章内容等。因为这个并发窗口期极短,且即便发生,TTL也会在短时间内纠正数据。

策略二:阿里的Canal + MQ 异步通知

对于微服务架构,或者需要对缓存更新进行精细化控制的场景,基于数据库binlog的异步通知是最佳实践。
  • 流程:应用只管更新数据库 -> Debezium/Canal 订阅数据库 binlog -> 将数据变更消息发送到 MQ(Kafka/RocketMQ)-> 一个专门的缓存同步服务消费消息,并对缓存进行精准的更新或删除
  • 优点:应用与缓存管理完全解耦、高可用(MQ保证消息不丢失)、可追溯、性能影响小。
  • 适用场景:需要保证最终一致性、系统间交互复杂、流量巨大的核心业务。如电商系统的商品信息同步。

策略三:版本号机制(应对“配置类”数据的利器)

当更新的数据是配置、规则等重要信息时,我们不希望出现新旧版本数据混杂的情况。
  • 流程:在缓存的数据中增加一个版本号字段(或直接用时间戳)。每次更新数据库时,版本号递增。应用读取缓存时,可以校验版本号;或者在更新缓存时,直接用新版本数据覆盖旧版本。
  • 优点:能有效防止旧数据被错误地写回缓存,控制更精确。
  • 适用场景:金融风控规则、系统配置参数、AB实验策略等。

策略四:不缓存(终极一致性方案)

当遇到像金融余额、电商库存这类绝对不能出现不一致的数据时,最简单、最安全的策略就是:放弃缓存
  • 流程:所有的读、写操作,全部直接穿透到数据库,并利用数据库事务来保证其原子性和一致性。
  • 优点:强一致性。
  • 适用场景:金融级核心数据、库存管理等对数据精确性要求100%的场景。

结论:务实胜于完美

回到最初的问题:为什么“延迟双删”在真实业务中很少见?
答案是:因为它试图用一种复杂的手段,去解决一个在大部分场景下并不严重、且有更多简单可靠方案可以替代的问题。
在真实的工程世界里,我们永远在做权衡。简单可靠、易于维护、能满足业务需求的方案,永远优于那个理论上完美无瑕但实施起来却举步维艰的“银弹”。
下一次,当面临缓存一致性的挑战时,希望你能抛开“延迟双删”的思维定式,从业务的实际需求出发,在上述这些久经考验的策略中,做出最恰当的选择。
“超参数”:从机器学习理论到广告竞价系统广告链路梳理
Loading...