尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

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

RocketMQ 延迟消息实战:延迟双删策略解决 Redis 缓存一致性
📅 发布时间:2026/8/2 2:15:59

大家好,我是晚安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:

cdrocketmq-all-5.3.4-bin-releasenohupshbin/mqnamesrv&tail-f~/logs/rocketmqlogs/namesrv.log

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

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

启动 Broker:

nohupshbin/mqbroker-nlocalhost: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 先把消息放进调度队列,到点才投递给消费者。你可以理解成「外卖定时送达,到点才敲你门」。

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

DefaultMQProducerproducer=newDefaultMQProducer("order_producer");producer.setNamesrvAddr("localhost:9876");producer.start();// 延迟 30 秒投递:第 4 级 = 30sMessagemsg=newMessage("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 里是这么流转的:

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

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

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

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

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

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

延迟双删的思路就三步:

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

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

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

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

// 写库 + 第一次删缓存 + 发延迟消息orderMapper.update(order);redis.del("order:"+order.getId());MessageflushMsg=newMessage("cache-flush",order.getId().toString().getBytes());flushMsg.setDelayTimeLevel(4);// 30s 后再删一次producer.send(flushMsg);
// 消费者:收到延迟消息 = 时间窗结束,执行第二次删除publicclassCacheFlushListenerimplementsRocketMQListener<Message>{@OverridepublicvoidonMessage(Messagemessage){StringorderId=newString(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.conf的messageDelayLevel里,想加等级自己也能改。


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

相关新闻

  • Word表格高效生成序号的4种方法:从编号、公式到域代码全解析
  • 2026年8月湖南省邵阳市电信单宽带避坑全攻略 - 找卡家园
  • GD32F403 EXMC外部存储器控制器配置与调试全攻略

最新新闻

  • 腾讯位置大数据实战:从API获取到可视化分析的完整数据清洗流程
  • XML标签提示法:用标签结构化复杂指令
  • 北大图灵班启示录:顶尖计算机人才如何构建代码之外的“元能力”
  • 2026年8月上海市闵行区移动单宽带怎么选_一篇说透 - 找卡家园
  • C++异常处理深度解析:从terminate报错到健壮代码设计
  • 2026年国内专业的工业测温仪厂家对外电话推荐 - 品牌排行榜

日新闻

  • 怀化母婴除甲醛公司测甲醛中心怎么选:康之居母婴除甲醛标准、流程、避坑指南 - 信誉隆金银铂奢回收
  • 三步打造你的终极音乐中心:foobox-cn网络电台功能完整指南
  • Lance湖仓格式:为多模态AI工作流设计的终极数据存储方案

周新闻

  • 怀化母婴除甲醛公司测甲醛中心怎么选:康之居母婴除甲醛标准、流程、避坑指南 - 信誉隆金银铂奢回收
  • 三步打造你的终极音乐中心:foobox-cn网络电台功能完整指南
  • Lance湖仓格式:为多模态AI工作流设计的终极数据存储方案

月新闻

  • ClickHouse版本管理深度实战:4步构建零风险升级与回滚体系
  • Java 23 种设计模式:从踩坑到精通 | 番外:责任链模式 —— 物流审批流程实战
  • 华硕笔记本性能解放指南:G-Helper轻量级控制工具全面解析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号