Redis延时双删详解
先来看一段代码:
// 写操作:更新DB + 删除缓存(延时双删)@Transactionalpublic void updateUser(User user) {// 1. 先更新数据库userMapper.update(user);String key = buildCacheKey(user.getId());// 2. 立即删除缓存redisTemplate.delete(key);// 3. 延时双删,避免并发脏读executor.submit(() -> {try {Thread.sleep(500); // 延迟0.5秒redisTemplate.delete(key);} catch (InterruptedException ignored) {}});}
不禁要问:Tell me why?
下面用实例时间线 + 原因 + 解决思路 来讲,越具体越直观。
核心结论(先给个一句话)
当写操作 先更新 DB 再删缓存
时,有可能会有并发的读操作在你删缓存之前或之后去读到旧数据并把旧值写回缓存,导致缓存里最终是旧(脏)数据。延时双删就是在「立刻删一次」之后再等短时间再删一次,把这些并发读写回来的脏值再刪掉,增加最终一致性的概率。
用具体时间线说明(最常见的一种情况:DB 主从复制 / 读副本延迟)
设:
旧值为
O
,新值为N
。写线程为 W,读线程为 R。
初始:DB 主/从还没同步,cache 可能不存在(miss)或已过期。
时间线举例:
t0: DB 主 = O,缓存 = (不存在或 O)
t1: W 开始执行 update -> 把 DB 主 更新为 N (还没完成从库同步)
t2: R 发起读取:cache miss -> 去读 DB,但正好读到的是从库(还没同步到 N) -> 读到 O -> R 将 O 写回缓存(cache = O)
t3: W 删除缓存(第一次删除),此时如果 R 的写已经发生,缓存可能已被设成 O;如果 R 的写还在稍后发生,则缓存又会被覆盖成 O
t4: W 启动延时任务(比如 500ms 后)再删一次缓存(第二次删除)
t5: 延时删除生效,删除掉 R 写回来的旧值 O(cache 变为空),后续读会从 DB(现在 DB 主有 N)回填 N
关键点:
R 能读到
O
的原因通常是读到的不是刚更新的主库数据(例如读到了尚未同步的从库),或者 R 在很早就已开始的长事务里读取到了旧快照。如果 R 在你第一次删除缓存 之后 写回了旧值(因为读的是旧数据),那么单次删除就“失效”了 —— 缓存又被旧值覆盖。延时第二次删除就是为了覆盖这个窗口期,把旧值再删掉一次,保证最终一致。
什么时候更容易出现这种问题?
数据库主从是异步复制(从库有延迟)。
读走的是从库以减轻主库压力(常见的读写分离架构)。
读操作处于旧事务或慢查询,读到旧快照。
系统中有很多并发读/写,时间窗口容易被触发。
为什么不是“永远有效”的万灵药?
延时双删是经验性/幂等防御措施,能显著降低脏数据概率,但不是数学上绝对保证。
如果延迟时间选择不当(太短可能没覆盖到窗口,太长会增加额外抖动和删除开销),效果会受限。
如果某些读写非常慢或复制延迟很大,单次延时可能不足——需要配合重试或更强的设计。
更稳妥的替代或补充方案(按强一致性程度排序)
分布式锁(写的时候锁住该 key,阻止并发的读写回填)——严格但有性能代价、死锁风险要处理。
版本号/时间戳:把版本或
updated_at
一并写入缓存;读时比对 DB(或用乐观校验),如果缓存版本落后则刷新。写入缓存而不是删除(Write-through / Write-behind):写操作同时更新 cache(或由 cache 组件同步到 DB),避免 delete->race,但实现复杂。
把删除操作放到可靠队列(MQ):DB 更新后把“删缓存”操作发到 MQ,确保最终能删除(防止进程宕机导致没删)。
用强一致性存储(如把热点放在强一致性的 DB/服务):牺牲性能换一致性。
本地缓存 + pub/sub 通知(多实例一致性):更新时通知其它实例清本地缓存,减少跨实例脏数据。
实际工程中的改进建议(实践可落地)
采用
更新 DB -> 立即删除 cache -> 延时再删除一次
(例如 200ms–1s,根据你系统的复制延迟/并发情况调参)。把第二次删除做成异步可重试的任务(线程池/定时任务/发 MQ),避免删除失败丢失。
对关键(强一致性必须)数据使用分布式锁或版本号策略。
对缓存回填(读端)做幂等处理或限制:如果读取 DB 的结果比写入缓存的旧(检查 version),就不要写回缓存。
在高并发场景做压测,测出“脏数据窗口”,据此设置延时值与重试策略。
小结(一句话回顾)
延时双删是一个 工程化的折中:在
update DB -> delete cache
的基础上,再等一小段时间再删一次,以清除那些在你删除后、读并写回旧值的并发读造成的脏缓存。它降低了脏数据概率,但不是绝对保证——对严格一致性的场景应考虑锁、版本号或强一致性方案。