RocketMQ 延迟消息实战:延迟双删策略解决 Redis 缓存一致性

简介: RocketMQ 延迟消息怎么用?本文从本地搭建开始,用订单超时案例讲透延迟消息与延迟双删,解决 Redis 缓存一致性难题。

大家好,我是晚安code。

更新完数据库,缓存里却还躺着旧数据——这种脏数据我太熟了。上周排查一个订单价格对不上的线上问题,最后定位到就是「先删缓存再写库」在并发下翻了车。

这篇我把 RocketMQ 延迟消息和延迟双删(Delay Double Deletion)讲透:先带你本地搭一套 RocketMQ,再用延迟消息让「第二次删除」准时到位,把 Redis 缓存一致性这个老大难摁下去。

点个收藏,咱们开始。

一、Redis 缓存为什么会有一致性问题

缓存与数据库的一致性问题,本质是一场「谁先动手」的并发赛跑,谁跑赢了,数据就听谁的。

说真的,缓存这东西就是来扛并发的——读多写少的业务,把热点数据扔进 Redis,数据库的压力能降一个量级。但它也带来一个新麻烦:同一份数据,库里存一份,缓存里存一份,两边怎么保证不打架?

先看最常见的写法:

Cache Aside(旁路缓存):最常用的缓存读写模式,读请求先查缓存,查不到再查数据库并回填;写请求更新数据库后删除缓存。你可以理解成「用到才取,改完就作废」。

那「改完就作废」为什么不直接更新缓存、非要删掉?因为更新缓存得先把新值算出来,还得防着并发写覆盖,删掉让下次读的时候重新回填,逻辑最省事。

问题出在「删缓存」和「更新数据库」不是原子的。你删完缓存,另一个线程刚好读到数据库里的旧值,在你反应过来之前就把旧值塞回缓存了,脏数据就这么诞生:

Redis 缓存一致性(Redis Cache Consistency):指缓存里的数据和数据库里的数据保持一致这个属性。你可以理解成「图书馆台账和实际馆藏对得上」。

打个比方,图书馆管理员把旧借阅登记撕了,准备重新誊抄新账本,结果誊抄之前有个读者照旧登记又抄了一遍。台账和馆藏,就这么对不上了。

删一次,旧值趁虚而入;删两次,缓存才干净

所以光删一次是不够的——脏数据可能在你删完的下一秒就被人塞回来。这就要引出今天的正题:删第二次,而且还要「延迟」删第二次。

二、RocketMQ 本地搭建:两个进程跑起来

本地跑 RocketMQ 用不着集群,NameServer + Broker 两个进程就够(2026 年 8 月用 5.3.4 实测)。

RocketMQ(Apache RocketMQ):Apache 基金会开源的分布式消息中间件,负责把消息从生产者可靠地送到消费者。你可以理解成「带保险的快递驿站,件在、单号在、签收有记录」。

RocketMQ 里有两个核心角色:NameServer 管路由,记录每个 Broker 在哪;Broker 管存储,真正存消息。启动顺序是先 NameServer,再 Broker。

前置条件:JDK 1.8+(64 位);从 rocketmq.apache.org 下载二进制包并解压,我写这篇时最新是 5.3.4,命令以你实际版本为准。

启动 NameServer:

cd rocketmq-all-5.3.4-bin-release
nohup sh bin/mqnamesrv &
tail -f ~/logs/rocketmqlogs/namesrv.log

日志里看到 The Name Server boot success... 就说明启动成功。

我第一次搭的时候在这翻过一次车——broker 启动得比 namesrv 还快,结果报 nameserver is not ready yet。记住了:先 namesrv,等日志跑起来,再开 broker。

启动 Broker:

nohup sh bin/mqbroker -n localhost:9876 &
tail -f ~/logs/rocketmqlogs/broker.log

日志里看到 The broker[broker-a, IP:10911] boot success... 就说明启动成功。

到这儿一套单机 RocketMQ 就活了。两个端口记住:9876 是 NameServer,10911 是 Broker,后面代码里要用。

想看消息长啥样,可以再起一个 rocketmq-dashboard 控制台:clone 官方仓库后 mvn spring-boot:run,浏览器开 http://localhost:8080,把 namesrvAddr 填成 localhost:9876,Topic 和消息一目了然。

Windows 的话给个提醒:官方文档推荐 64 位 Linux/Mac 跑 sh 脚本,Windows 上要么装 WSL2,要么用 Docker 起容器,别硬刚原生环境,坑多。

三、RocketMQ 延迟消息:给消息定个闹钟

延迟消息就是给消息装了个闹钟——到点了才响,消息到点了才投递。

RocketMQ 延迟消息(Delayed Message):发送时指定延迟等级,Broker 先把消息放进调度队列,到点才投递给消费者。你可以理解成「外卖定时送达,到点才敲你门」。

用法和普通消息几乎一样,只在发送时多设一个延迟等级:

DefaultMQProducer producer = new DefaultMQProducer("order_producer");
producer.setNamesrvAddr("localhost:9876");
producer.start();

// 延迟 30 秒投递:第 4 级 = 30s
Message msg = new Message("order-timeout",
        "cancel", "order-1001".getBytes());
msg.setDelayTimeLevel(4);
producer.send(msg);

不过有个反直觉的坑:延迟等级是写死的 18 级,不是随便填秒数:

1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

想延迟 3 分钟?抱歉,没有这个档位,你只能挑 2m 或 4m。这个设计是故意的——任意秒数会让 Broker 做全局消息排序,性能扛不住,所以用固定梯度换吞吐。

电商下单 30 分钟未支付自动取消,就是延迟消息的经典用法:下单成功发一条 level=16(30 分钟)的消息,消费者到点查订单,还没支付就取消、释放库存。30 分钟一到,闹钟准时响。

消息在 Broker 里是这么流转的:

RocketMQ 延迟消息流转图:生产者带延迟等级发送,Broker 先入调度队列,到点投递业务 Topic,消费者再消费

底层机制一句话:延迟消息先落到 SCHEDULE_TOPIC_XXXX 调度主题,18 个队列对应 18 个等级,Broker 的定时任务到点把它们转投到真正的业务 Topic。

可能有人会问:延迟消息能精确到秒吗?

不能。开源版只有这 18 个固定等级,最大 2 小时,投递还会带 1~2 秒误差。真要任意秒级精度,得自己改源码扩展调度逻辑,或者用云厂商的延迟消息版本。

四、延迟双删(Delay Double Deletion):让第二次删除等一等

普通双删怕的不是删不掉,而是删完第一遍后,并发读又把旧值塞了回去。

延迟双删(Delay Double Deletion):更新数据库后先删除一次缓存,隔一段延迟时间再删除一次缓存,把并发读回写造成的脏数据再清一遍。你可以理解成「撕了旧公告,等人都看完,再补撕一次残留」。

延迟双删的思路就三步:

  1. 更新数据库
  2. 删除缓存(第一次)
  3. 等一段延迟(覆盖并发读回写脏数据的窗口),再删除缓存(第二次)

全流程画成时序图,一眼就懂:

RocketMQ 延迟消息实现的延迟双删时序图:更新数据库、第一次删缓存、发送延迟消息,延迟到期后第二次删缓存,清掉并发读回写的脏数据

我以前也觉得,删两次总够了吧?结果自己写了个并发 demo 一压,发现第一删和第二删之间,读线程照样能塞旧值进来——第二删不延迟的话,等于白删。这就是「延迟」两个字的关键:给并发窗口盖棺,而不是跟它赛跑。

第二次删除怎么实现?最省事的写法是主线程 sleep 几秒再删——但进程一重启就全没了。我强烈建议交给 RocketMQ 延迟消息,消息落盘、不丢、能重试、还能看消费记录:

// 写库 + 第一次删缓存 + 发延迟消息
orderMapper.update(order);
redis.del("order:" + order.getId());

Message flushMsg = new Message("cache-flush",
        order.getId().toString().getBytes());
flushMsg.setDelayTimeLevel(4);   // 30s 后再删一次
producer.send(flushMsg);
// 消费者:收到延迟消息 = 时间窗结束,执行第二次删除
public class CacheFlushListener implements RocketMQListener<Message> {
   
    @Override
    public void onMessage(Message message) {
   
        String orderId = new String(message.getBody());
        redis.del("order:" + orderId);   // 第二次删除缓存
    }
}

这中间有个判断要做:延迟多久?太短盖不住并发窗口,太长等于容忍长时间脏数据。我一般从「一次读请求从数据库回填缓存的平均耗时」往上翻几倍,再配合压测调,通常 1~5 秒够用。

写库→删缓存→等闹钟→再删,第二删给并发窗口盖棺

顺手把主流的几个方案放在一起对比,你心里就有数了:

方案 实现方式 脏数据窗口 复杂度 适合谁
只删一次(Cache Aside 基础版) 更新 DB → 删缓存 可能持续到缓存过期 最低 并发极低、容忍脏数据
延迟双删 + RocketMQ 延迟消息 更新 DB → 删缓存 → 延迟消息 → 再删 秒级,可控 读多写少,能接受最终一致
订阅 binlog 异步刷新(如 Canal) 监听 DB binlog 驱动缓存删除/更新 毫秒级,接近实时 核心链路,一致性要求高

五、两个容易踩的坑

延迟双删不是银弹,坑主要藏在延迟时长和二次删除失败上。

坑 1:延迟时长拍脑袋。 延迟设太短,并发窗口没盖住,第二删等于白删;设太长,脏数据会直接展示给用户。正确做法是先量:压测里读线程把旧值写回缓存要多久,延迟取这个值的 2~3 倍打底,再观察线上告警有没有收敛。

坑 2:第二次删除失败了怎么办。 消费失败 RocketMQ 默认会重试(16 次),一般够用。但网络抖动、Redis 挂了都可能导致最终没删掉,缓存里的脏数据会一直活到过期。所以生产环境我会再加一道兜底:二次删除也失败,就投一条更长延迟的补偿消息;同时让缓存 TTL 别设太长,当作最后的保险。

可能有人会问:第二次删除失败,数据会一直脏下去吗?

不会永久脏,缓存有 TTL,到点自己失效,下次读会回填新值。问题是这个窗口可能很长。所以关键不是「绝对不失败」,而是「失败了能被发现、有兜底」——重试 + 补偿消息 + 合理 TTL,三件套配上基本就稳了。

六、小结:延迟双删不是银弹

延迟双删给的是「最终一致」,不是「绝对一致」——这句话决定了你该不该用它。

说句大实话,RocketMQ 延迟消息 + 延迟双删是我现在做缓存一致性最顺手的一套组合:本地 20 分钟能搭起来,改动只有「多删一次缓存 + 发一条延迟消息」,却把最脏的那个并发窗口堵住了。但它换来的只是最终一致,如果业务要求读到的一定是最新数据,比如余额、库存超卖这种,该上 binlog 订阅或者分布式锁,别硬用缓存。

想深入的话,官方文档(搜:RocketMQ 官方文档)里有延迟消息和部署的完整说明;延迟等级对应关系在 broker.confmessageDelayLevel 里,想加等级自己也能改。


我是晚安code,持续分享编程干货。觉得有用的话记得点赞收藏和关注~也欢迎在评论区聊聊:你踩过缓存脏数据的坑吗?会用 RocketMQ 延迟消息做延迟双删吗?

目录
相关文章
|
4天前
|
设计模式 算法 Java
干掉成山的 if-else:工厂造、策略选,一文讲透两个模式的配合
写给被成堆 if-else 折磨的后端同学:拆开工厂模式和策略模式,搞懂创建与选择的解耦,配合 Spring 实战消灭分支地狱。
54 1
干掉成山的 if-else:工厂造、策略选,一文讲透两个模式的配合
|
2月前
|
前端开发 Java Nacos
Nacos 注解全解析:7 个核心注解 + 5 个生产踩坑清单(2026 实测)
Nacos 注解用错,是 90% 微服务 Bug 的根源。本文把注册发现、配置热更新的 7 个核心注解、动态刷新原理和 5 个生产踩坑一次讲透,附可运行代码。
346 2
Nacos 注解全解析:7 个核心注解 + 5 个生产踩坑清单(2026 实测)
|
5月前
|
存储 人工智能 关系型数据库
OpenClaw怎么可能没痛点?用RDS插件来释放OpenClaw全部潜力
OpenClaw插件是深度介入Agent生命周期的扩展机制,提供24个钩子,支持自动注入知识、持久化记忆等被动式干预。相比Skill/Tool,插件可主动在关键节点(如对话开始/结束)执行逻辑,适用于RAG增强、云化记忆等高级场景。
1291 56
OpenClaw怎么可能没痛点?用RDS插件来释放OpenClaw全部潜力
|
5月前
|
缓存 网络安全 数据安全/隐私保护
Socks5代理使用避坑指南,常见问题及应对策略汇总
本文详解Socks5代理五大高频问题(连接失败、无法上网、卡顿断连、IP被封、软件不兼容)及零门槛实操解法,涵盖参数核对、节点切换、协议设置、IP轮换等技巧,无需专业术语,新手一看就会,助你稳定高效使用代理。
1112 11
|
5月前
|
监控 Java 测试技术
Spring Boot学习知识点大全(三)
教程来源 https://app-a6nw7st4g741.appmiaoda.com/ 系统梳理Spring Boot核心实践:涵盖日志分级配置与异步输出、单元/集成测试、Actuator监控与自定义指标、Docker/K8s部署、Spring Boot 3.x Jakarta迁移及虚拟线程等新特性,助力构建高可用生产级应用。
|
3月前
|
人工智能 运维 架构师
我在 AIP 智能体平台踩过的坑,都在这篇企业 AI 落地经验里了
软件架构师罗小东分享企业AI落地实战经验:聚焦AIP智能体平台建设中的真实坑点与解法——涵盖智能体全生命周期管理、多源知识库语义检索、MCP工具集成及多模型中立架构设计,强调“解决问题”而非堆砌功能。(239字)
|
4月前
|
Java 大数据 双11
一张图看懂 Java 能干什么——从淘宝下单到双11抢货,背后都是它
本文专为Java零基础小白打造,用通俗比喻讲清Java本质(“万能翻译官”)、跨平台特性及核心优势;解析其在电商、支付等真实场景的应用;破除“Java已死”误区,结合数据说明其持续强势;并给出清晰入门路径与实用学习建议,助你科学起步。
一张图看懂 Java 能干什么——从淘宝下单到双11抢货,背后都是它
|
3月前
|
安全 API 开发者
语雀如何导出别人的知识库!超实用教程!支持导出markdown/html/word/pdf等超多格式!一键解析,批量下载!保留大纲的层级结构!一键克隆!原格式
语雀重度用户亲测推荐:「语雀文档批量下载克隆助手」浏览器插件。一键备份知识库、小记、表格、白板等全类型内容,完美保留目录结构与图片附件;支持导出MD/HTML/Word/PDF,并可克隆公开库至个人账号。100%本地运行,不传数据、无需Token或超级会员,安全可靠。(239字)
|
3月前
|
SQL 人工智能 安全
从 BI Copilot 到业务 Agent:指标服务如何成为统一数据接口?
一个能够提供标准化、语义化、服务化数据接口的指标层,正成为数据智能新阶段的战略基础设施。

热门文章

最新文章