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/
作者
clxstart
发布于
2026-10-10
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。