大厂是如何优雅避开“延迟双删”的?

117 阅读4分钟

沉默是金,总会发光

大家好,我是沉默

在很多系统设计方案里,延迟双删几乎成了「缓存一致性」的标准解。
数据库更新 → 删除缓存 → 等几秒 → 再删一次。

看似完美,但真相是:
~对小系统,它简单高效;
~对大流量系统,它可能引爆缓存击穿,让主库崩溃。

这就像打疫苗:剂量没错,但体质不同,效果可能完全相反。

今天我们就从原理、缺陷到阿里的实战方案,一次讲透「缓存一致性」的现代解法。

**-**01-

延迟双删为什么诞生?

问题的起点在于「缓存和数据库之间的时间差」。

假设你刚更新了数据库记录,但缓存里还是旧数据。
用户在这几毫秒的时间窗口里访问缓存,就会读到过期数据。

于是聪明的工程师想了个办法:

“我删除两次缓存,第一次在写库前后删,第二次延迟1~2秒再删一次。”

这样能覆盖绝大多数「并发写 + 读缓存」的情况。

但也因此引出了两个大坑:

  • 延迟时间不好调(太短没用,太长不一致);
  • 第二次删除容易引起缓存击穿,直接把主库压垮。

这就是延迟双删的「双刃剑」本质。

图片

- 02-

致命缺陷:流量击穿

假设你的系统高峰期有几千 QPS。
延迟双删后短暂失效的缓存会让这些请求全部打到数据库。

瞬间的「缓存空窗期」= 主库压力暴增。

结果?
数据库响应慢 → 用户疯狂刷新 → 流量更大 → 主库雪崩。

这正是延迟双删在大流量系统中被抛弃的原因。

阿里就踩过这个坑。

图片

- 03-

大厂解法

1. 租约(Lease)机制 它本质上是给「谁能写缓存」加上了一个“令牌”。

原理简述

  1. 多个请求同时查询缓存,缓存 miss。
  2. 缓存返回一个 token(租约),只允许第一个请求持有它。
  3. 只有持有租约的请求能写入缓存。
  4. 其他请求要么等租约过期,要么丢弃结果。

这就避免了并发写导致的旧数据覆盖问题。

你可以把它理解为“写缓存的排他锁”,
但比数据库锁更轻量、可控。

Java + Lua 简易实现思路

  • lease:get:当 key 不存在时,生成租约 token。
  • lease:set:验证 token 是否匹配,再允许写缓存。
  • lease:del:删除缓存和对应租约,防止旧写入回流。

这些逻辑用 Redis 的 Lua 脚本实现,原子又安全。

图片

2. 版本号比对机制 另一种思路:不用租约,用版本号。

原理简述

每条数据都带有版本号(通常是时间戳)。
写入缓存前,比对 Redis 中的版本号:

  • 如果新版本号 > 旧版本号,才更新缓存;
  • 否则丢弃写入。

从而保证缓存中永远是最新数据。

实现要点

  • 在 Lua 脚本中用 mset 同步写入版本号和数据;
  • 应用层抽取时间戳并传入 Redis;
  • 仅当版本号有效时,脚本返回 effect=true

这种方式在高并发下比租约更轻量,更适合阿里那种上亿级访问场景。

3. 对比分析:两大方案的取舍

方案适用场景核心机制优点缺点
延迟双删中小系统删除缓存 + 延迟删除简单易用可能引发击穿
Lease大流量系统租约令牌控制并发写防并发写入实现复杂
Version超大规模系统版本号比对更新无需锁,性能高依赖精确版本控制

延迟双删更像是“小团队的瑞士军刀”;
而租约与版本号方案,则是“互联网巨头的精密仪表”。

**-**04-

总结

每种方案都有生命周期。
1. 延迟双删适合流量低、成本敏感的系统;
2. 租约机制适合分布式高并发写场景;
3. 版本号机制则是超大规模、高一致性系统的终极解。

别盲目模仿大厂。
因为他们优化的是「亿级 QPS」,
而你要的是「稳定 + 可控 + 成本可接受」。

延迟双删不是错,它只是“进化的起点”。
当系统规模上升,延迟、并发、缓存击穿的问题就会被放大。

真正成熟的系统设计,不是追求完美一致性,
而是在一致性、性能、成本之间,找到最适合自己的平衡点。

**
**

延伸阅读推荐:

  • 《系统设计实战:缓存一致性与分布式模式详解》系列

图片

你在项目中是如何保证缓存一致性的?
留言区聊聊你踩过的坑,或者你的“低成本真香方案”

**-**05-

粉丝福利

点点关注,送你互联网大厂面试题库,如果你正在找工作,又或者刚准备换工作。可以仔细阅读一下,或许对你有所帮助!

image.png

image.pngimage.png