引言
在高并发系统中,Redis 凭借高性能成为缓存首选。但你是否遇到过这样的问题:数据库更新了,但 Redis 读取的还是老数据?
今天我们就深入剖析高并发场景下的数据一致性问题,并揭示一种被广泛推荐的解决方案 ——
缓存双删策略,帮你彻底解决 Redis
与数据库不同步的问题!
📌一、为什么 Redis 与数据库会出现不一致?
典型的场景如下:
// 1. 先删除缓存
redis.del("user:1001");
// 2. 再更新数据库
update user set name = 'Tom' where id = 1001;
看似完美,但问题出在并发!
⚠️二、三种常见写操作流程及其并发漏洞分析
🔴 场景一:先更新数据库,再删除缓存
update user set name = 'Tom' where id = 1001;
redis.del("user:1001");
并发流程图:
| 时间 | 线程A(读) | 线程B(写) |
|---|---|---|
| T1 | 读取缓存,命中旧值 | |
| T2 | 更新数据库 | |
| T3 | 删除缓存 | |
| T4 | 返回旧缓存给用户 |
❌ 缓存中的旧数据未被及时清除,用户读到脏数据!
🟡 场景二:先删除缓存,再更新数据库
redis.del("user:1001");
update user set name = 'Tom' where id = 1001;
并发流程图:
| 时间 | 线程A(读) | 线程B(写) |
|---|---|---|
| T1 | 删除缓存 | |
| T2 | 读取缓存为空 | |
| T3 | 读取数据库旧数据 | |
| T4 | 写入旧数据到缓存 | 更新数据库 |
❌ 缓存被写回旧数据,数据库是新数据,数据不一致!
🔵 场景三:先删除缓存 → 更新数据库 → 延迟再删缓存(缓存双删)✅
redis.del("user:1001");
update user set name = 'Tom' where id = 1001;
Thread.sleep(500);
redis.del("user:1001");
并发流程图:
| 时间 | 线程A(读) | 线程B(写) |
|---|---|---|
| T1 | 删除缓存 | |
| T2 | 读取缓存为空 | |
| T3 | 查数据库(旧数据) | 更新数据库 |
| T4 | 写入旧数据到缓存 | 延迟 500ms 后再次删除缓存(清除脏数据) |
✅ 缓存虽然短暂写入了旧数据,但被延迟删除彻底清除,最终一致!
✅ 三、为什么推荐缓存双删策略?
- 第一次删除缓存: 减少读到旧数据的风险
- 第二次延迟删除: 清除被并发请求误写入的旧缓存
- 最终保证: Redis 与数据库数据最终一致
🔍 四、完整 Java 实战示例
public void updateUser(User user) {
String redisKey = "user:" + user.getId();
// 第一次删除缓存
redisTemplate.delete(redisKey);
// 更新数据库
userMapper.updateById(user);
// 异步线程延迟再次删除缓存(避免主线程阻塞)
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(500); // 延迟500ms
redisTemplate.delete(redisKey);
} catch (InterruptedException e) {
log.error("延迟删除缓存失败", e);
}
});
}
🧠 五、实践注意事项
| 问题 | 建议 |
|---|---|
| 延迟多长时间? | 300ms~1s,避免主从同步延迟 |
| 双删失败怎么办? | 使用补偿机制,或记录日志重试 |
| 多实例服务是否有效? | ✅ 有效,延迟删除由每个实例独立控制 |
📚 六、还有哪些一致性方案?
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 延迟双删 | 最简单有效 | 常规场景 |
| Canal + Binlog 监听 | 实时订阅数据库变更 | 高一致性需求 |
| 异步 MQ 通知清缓存 | 确保每次更新都清除缓存 | 分布式场景 |
| 分布式事务(TCC/SAGA) | 保证全链路一致性 | 跨服务更新场景 |
✅ 七、总结
缓存是性能优化的利器,但数据一致性不能忽视。
- 更新数据库前后执行两次删除缓存(双删策略)
- 在高并发下,避免旧数据回写 Redis
- 适配大多数场景,简单、实用
📢 如果你觉得本文有帮助,欢迎点赞收藏 👍

被折叠的 条评论
为什么被折叠?



