Redis延时双删
在“Redis 缓存 + MySQL 数据库”的经典架构中,数据一致性是高频痛点。延时双删作为一种实现简单、成本低廉、适配多数最终一致性场景的解决方案,被广泛应用于非核心业务(如商品详情、用户历史记录)。
# 一、为什么需要“延时双删”?
在聊延时双删前,先明确一个核心矛盾:Redis 与 MySQL 是独立存储系统,读写操作无法原子化完成,并发场景下极易出现数据不一致。我们先看两个经典错误场景,理解延时双删的诞生意义。
# 1.1 错误场景1:先更数据库,再更缓存
- 流程:
- 线程A 更新 MySQL,将值从 V1→V2,数据库现在是 V2
- 线程B 更新 MySQL,将值从 V2→V3,数据库现在是 V3
- 线程B 更新 Redis,将值从 V1→V3,Redis现在是 V3
- 线程A 更新 Redis,将值从 V3→V2,Redis现在是 V2
- 问题:并发修改时,MySQL 是新数据,Redis 保留旧数据,形成不一致,直到缓存过期。
# 1.2 错误场景2:先删缓存,再更数据库(无延时)
- 流程:
- 线程A 删除 Redis 缓存;
- 线程A 未完成 MySQL 更新,事务为提交;
- 线程B 读取 Redis 缓存未命中,从 MySQL 读取旧数据并回写缓存;
- 线程A 完成 MySQL 更新。
- 问题:Redis 被旧数据“污染”,后续请求会持续读取错误数据,直到缓存过期。
# 1.3 延时双删的核心价值
延时双删通过“两次删除缓存 + 中间延时等待”,精准解决上述并发时序问题,核心目标是让缓存数据最终收敛到与数据库一致,同时兼顾实现成本与性能。
# 二、延时双删核心原理与实现
# 2.1 三步核心流程
延时双删的操作逻辑极简,仅需三步,就能规避并发读写导致的缓存污染:
- 第一次删除:更新数据前,先删除 Redis 对应缓存,让后续读请求暂时穿透到数据库;
- 更新数据库:正常写入/更新 MySQL 数据,保证持久化存储的准确性;
- 延时等待后第二次删除:等待一段时间,再次删除 Redis 缓存,清除可能被线程B 回写的旧数据。
# 2.2 关键逻辑拆解
第二次删除的“延时等待”是核心,其目的是:等待所有在“第一次删缓存后、更数据库前”发起的读请求,都完成旧数据的读取与回写,再通过第二次删除清空这些旧数据。后续读请求会从数据库读取新数据,重新回写缓存,达成最终一致。
# 2.3 代码落地实现
// 延时双删核心方法(更新数据场景)
public void updateData(Long id, String newData) {
// 缓存键(按业务设计,如前缀+ID)
String cacheKey = "data:" + id;
// 第一步:第一次删除缓存
redisTemplate.delete(cacheKey);
try {
// 第二步:更新 MySQL 数据库(实际业务中需加事务)
updateMySQL(id, newData);
} catch (Exception e) {
// 数据库更新失败,可回滚缓存(可选,视业务容错性)
redisTemplate.opsForValue().set(cacheKey, getOldDataFromMySQL(id), 30, TimeUnit.MINUTES);
throw new RuntimeException("数据库更新失败", e);
}
// 第三步:延时 1 秒后,第二次删除缓存
executorService.schedule(() -> {
redisTemplate.delete(cacheKey);
}, 1, TimeUnit.SECONDS);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 三、关键参数:延时时间怎么定?
延时时间的设定直接影响一致性效果与性能,不能凭感觉定,需结合三个核心因素:
- 数据库事务耗时:若更新操作涉及复杂事务(如多表关联),需预留足够时间让事务提交完成;
- 业务 QPS 与并发量:高并发场景下,读请求回写缓存的速度更快,可适当缩短延时;低并发场景可延长,避免过早删除;
- 网络延迟:Redis 与 MySQL 部署在不同节点时,需考虑网络传输耗时。
# 推荐取值与校准方法
- 常规场景:默认 1-3 秒,覆盖大部分中小并发业务;
- 高并发场景:0.5-1 秒,减少缓存穿透时间,降低数据库压力;
- 复杂事务场景:3-5 秒,确保事务完全提交后再二次删除。
# 校准技巧
上线后通过日志监控“第一次删缓存到第二次删缓存”期间的读请求量,若仍存在缓存污染,逐步增加延时时间(每次加 500ms),直到不一致问题消失。
# 四、延时双删的优缺点与适用场景
# 4.1 优点
- 实现简单:无需引入 Canal、消息队列等中间件,代码侵入性极低;
- 成本低廉:仅增加两次缓存删除操作,对性能影响可忽略;
- 容错性强:即使第二次删除失败,缓存设置了过期时间,也能兜底保证最终一致。
# 4.2 缺点
- 存在短时间不一致窗口:从第一次删缓存到第二次删缓存的间隙,仍可能读取到旧数据,不适用于超强一致性场景(如支付、库存);
- 延时时间难精准把控:不同业务场景下需反复校准,无统一标准;
- 高并发写场景有局限:频繁删除缓存会导致大量读请求穿透到数据库,可能引发数据库压力飙升。
# 4.3 适用与不适用场景
| 场景类型 | 是否适用 | 原因 |
|---|---|---|
| 商品详情、用户资料 | 适用 | 允许秒级不一致,追求高读性能 |
| 订单列表、历史记录 | 适用 | 数据变更频率低,最终一致即可 |
| 支付金额、库存数量 | 不适用 | 需强一致性,避免超卖、资金误差 |
# 五、实战避坑:这些问题一定要注意
# 5.1 缓存删除失败怎么办?
两次删除操作都可能因 Redis 宕机、网络超时失败,需做好兜底:
- 第一次删除失败:直接放弃更新操作,返回错误提示,避免“数据库新数据、缓存旧数据”;
- 第二次删除失败:借助异步重试机制(如消息队列),或依赖缓存过期时间兜底,同时监控删除失败率,超过阈值告警。
# 5.2 避免缓存穿透放大
延时双删会导致短时间内缓存穿透(第一次删后到第二次删前,读请求直接透库),高并发下需配合防护:
- 非核心数据:设置空值缓存(如缓存不存在的数据为“null”,过期时间 5-10 秒);
- 核心热点数据:结合互斥锁,同一时间仅允许一个线程读库回写缓存,避免数据库雪崩。
# 5.3 不要滥用延时双删
并非所有缓存更新场景都需要延时双删:
- 写少读多场景:优先用“先删缓存+缓存过期”,可省略第二次删除,减少复杂度;
- 数据变更频率极高场景:建议改用 Canal 异步同步方案,避免频繁删除缓存。
# 六、延时双删 vs 其他一致性方案
为帮你精准选型,对比主流缓存一致性方案的差异:
| 方案 | 一致性级别 | 实现复杂度 | 性能 | 适用场景 |
|---|---|---|---|---|
| 延时双删 | 最终一致性 | 低 | 高 | 中小并发、非核心业务 |
| 分布式锁+同步更新 | 强一致性 | 中 | 中 | 核心业务(库存、支付) |
| Canal+Binlog 异步同步 | 最终一致性 | 高 | 高 | 高并发、海量数据场景 |
| 先更库再更缓存 | 最终一致性 | 低 | 中 | 低并发、缓存更新成功率 100% 场景 |
# 七、最佳实践总结
- 必加缓存过期时间:无论延时双删多稳定,都要给缓存设置过期时间(5-30 分钟),作为不一致的最后兜底;
- 延时时间动态校准:上线后根据业务并发量、事务耗时调整延时时间,避免一刀切;
- 失败重试兜底:第二次删除失败时,通过消息队列异步重试,确保删除操作最终执行;
- 结合业务场景选型:非核心最终一致性场景用延时双删,核心强一致场景用分布式锁,高并发场景用 Canal 方案。
# 八、总结
Redis 延时双删是“性价比极高”的缓存一致性解决方案,核心优势在于简单落地、低侵入、高性能,完美适配多数非核心业务的最终一致性需求。其本质是通过“两次删除+延时等待”,巧妙规避并发读写的时序问题,同时借助缓存过期时间兜底,平衡一致性与性能。