1153 字
约 3 分钟
0
延时消息 + 延迟双删策略:保证 Redis 缓存一致性
在数据库与缓存双写的并发场景下,缓存很可能被"老数据"回填,造成脏读。本文讲解延迟双删策略的原理,以及如何用 RocketMQ 延时消息落地"第二次删除",保证 Redis 缓存最终一致。
一、要解决的问题:双写一致性
双写一致性:更新数据时,确保数据库和缓存两个存储系统中的数据保持一致,避免脏读。
以一个极端并发场景为例(此时 Redis 中笔记详情缓存恰好已失效):
① 查询接口:查 Redis 未命中 → 走数据库查出【老数据】(还没来得及回填 Redis)
② 更新接口:更新数据库 + 删除 Redis 缓存
③ 查询接口:此时才把刚才的【老数据】同步回填进 Redis ❌
→ 用户后续读取到的是老数据
问题本质:查询接口"查老数据 → 回填缓存"这个过程跨越了更新接口的执行时间点,把过期的老数据写回了缓存。
二、延迟双删策略
延迟双删(Delayed Double Deletion)通过两次删除缓存 + 延迟时间,尽可能减少因并发操作或主从同步延迟导致的脏数据残留。
先删 Redis 缓存
→ 更新数据库
→ 延时约 1s → 再删一次 Redis 缓存(防止老数据回填)
更新接口重构后的代码骨架:
// ① 先删缓存
String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
redisTemplate.delete(noteDetailRedisKey);
// ② 更新数据库
noteDOMapper.updateByPrimaryKey(noteDO);
// ③ 延迟双删:防止老数据回填(②③之间需延时约 1s)
redisTemplate.delete(noteDetailRedisKey);
为什么要"延迟"?
如果"删缓存 → 更新 DB → 删缓存"一瞬间就完成,而查询接口那时还没完成回填,第二次删除就会"扑空",缓存照样是老数据。延时 1s 再删第二次,就能覆盖绝大多数"查询接口完成回填"的时间窗口。
它并不能 100% 解决
如果查询接口查出老数据后耗时超过 1s,仍可能漏删。延迟双删是"尽可能减少脏数据残留",不是"绝对消除"——极端情况下,还有缓存过期失效来兜底。
三、用 RocketMQ 延时消息落地第二次删除
为了"延时 1s 再删",直接睡线程不可取(会阻塞接口),正确做法是借助 RocketMQ 的延时消息。
1. 声明 Topic
public interface MQConstants {
// Topic:延迟双删 Redis 笔记缓存
String TOPIC_DELAY_DELETE_NOTE_REDIS_CACHE = "DelayDeleteNoteRedisCacheTopic";
}
2. 异步发送延时消息(替代直接删)
Message<String> message = MessageBuilder.withPayload(String.valueOf(noteId)).build();
rocketMQTemplate.asyncSend(TOPIC_DELAY_DELETE_NOTE_REDIS_CACHE, message,
new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("## 延时删除 Redis 笔记缓存消息发送成功...");
}
@Override
public void onException(Throwable e) {
log.error("## 延时删除 Redis 笔记缓存消息发送失败...", e);
}
},
3000, // 超时时间(毫秒)
1 // 延迟级别:1 表示延时 1s
);
关键点:
- asyncSend 异步发送:不阻塞当前线程,提升接口响应速度;
- delayLevel 延迟级别:RocketMQ 4.x 的延时消息不是任意精确时间,而是通过预定义的 18 个延迟级别实现,每个级别对应固定延时时间,如等级 1 = 1s、等级 2 = 5s、等级 3 = 10s,更详细对应关系见官方文档。
3. 创建消费者:延时后删除缓存
@Component
@RocketMQMessageListener(
consumerGroup = "xiaohashu_group_" + TOPIC_DELAY_DELETE_NOTE_REDIS_CACHE,
topic = TOPIC_DELAY_DELETE_NOTE_REDIS_CACHE)
public class DelayDeleteNoteRedisCacheConsumer implements RocketMQListener<String> {
@Override
public void onMessage(String body) {
Long noteId = Long.valueOf(body);
// 删除 Redis 笔记缓存(延时约 1s 后执行)
String key = RedisKeyConstants.buildNoteDetailKey(noteId);
redisTemplate.delete(key);
}
}
延时消息投递下来后,消费者执行删除的正是那"第二次删除"。
四、总结
- 并发场景下,查询接口可能把"老数据"回填进 Redis 造成脏读,这是数据库与缓存双写一致性的经典难题;
- 延迟双删的核心套路:先删缓存 → 更新数据库 → 延时再删一次,第二次删除用于覆盖"老数据回填"的时间窗口;
- 它属于概率优化而非强一致:降低脏读概率,极端情况靠缓存过期兜底;
- 第二次删除用 RocketMQ 延时消息(asyncSend 异步发送 + delayLevel 延迟级别)实现,既延时又不阻塞接口;
- 注意 RocketMQ 延时消息是固定延迟级别(1s / 5s / 10s …),并非任意精确时间。
延时消息 + 延迟双删策略:保证 Redis 缓存一致性
https://www.clxhxhhr.top/posts/4595/ 评论
0 条
还没有评论,先写一条吧。