06-02-A-RocketMQ消息特性与消费机制详解
️ 关键词:顺序消息 · 延迟消息 · 事务消息 · 半消息 · 消息回查 · 集群消费 · 广播消费 · 消费重试 · 死信队列 · 消费幂等 · 消息过滤
📌 导读 :01 篇讲了 RocketMQ 的架构与存储(CommitLog+ConsumeQueue 读写分离),本篇讲在这个存储之上长出来的"消息特性"------顺序消息怎么保证端到端有序、延迟消息的 18 个等级怎么实现、事务消息的半消息+回查机制、消费模型(集群/广播)、重试与死信队列、消费幂等。这三种特色消息覆盖了分布式系统 90% 的异步场景,也是 RocketMQ 区别于 Kafka 的核心竞争力(Kafka 原生没有延迟消息和事务消息的业务级封装)。看完本篇你能做到:被问"顺序消息原理"能讲出"同 Key 同 Queue + 单 Queue 顺序消费"两保证,被问"事务消息原理"能画出半消息+回查流程,被问"消息重复怎么办"能给出幂等三板斧。
📑 目录
- 06-02-A-RocketMQ消息特性与消费机制详解
-
- [📖 术语速查表(每个词都用人话解释)](#📖 术语速查表(每个词都用人话解释))
- 一、顺序消息:两保证缺一不可
-
- [1.1 需求与分类](#1.1 需求与分类)
- [1.2 实现原理:发送有序 + 消费有序](#1.2 实现原理:发送有序 + 消费有序)
- [1.3 代码示例(本项目订单场景)](#1.3 代码示例(本项目订单场景))
- [1.4 顺序消息的两个坑](#1.4 顺序消息的两个坑)
- [二、延迟消息:18 个等级与内部 Topic](#二、延迟消息:18 个等级与内部 Topic)
-
- [2.1 需求与等级](#2.1 需求与等级)
- [2.2 实现原理:内部 Topic + 定时转发](#2.2 实现原理:内部 Topic + 定时转发)
- [2.3 代码示例(本项目订单超时取消)](#2.3 代码示例(本项目订单超时取消))
- [三、事务消息:半消息 + 回查](#三、事务消息:半消息 + 回查)
-
- [3.1 需求:本地事务与消息发送的原子性](#3.1 需求:本地事务与消息发送的原子性)
- [3.2 流程:半消息 + 本地事务 + 回查](#3.2 流程:半消息 + 本地事务 + 回查)
- [3.3 半消息的"偷梁换柱"](#3.3 半消息的"偷梁换柱")
- [3.4 代码示例(支付成功 → 加积分)](#3.4 代码示例(支付成功 → 加积分))
- [3.5 三个关键点](#3.5 三个关键点)
- 四、消费模型:集群与广播
-
- [4.1 两种消费模式](#4.1 两种消费模式)
- [4.2 Push 的本质:长轮询](#4.2 Push 的本质:长轮询)
- [4.3 并发消费 vs 顺序消费](#4.3 并发消费 vs 顺序消费)
- 五、消费重试与死信队列
-
- [5.1 重试机制](#5.1 重试机制)
- [5.2 重试次数与间隔对照](#5.2 重试次数与间隔对照)
- [5.3 死信处理的最佳实践](#5.3 死信处理的最佳实践)
- [六、消费幂等:MQ 使用的第一军规](#六、消费幂等:MQ 使用的第一军规)
-
- [6.1 为什么消息一定会重复](#6.1 为什么消息一定会重复)
- [6.2 幂等三板斧](#6.2 幂等三板斧)
- [七、消息过滤:Tag 与 SQL92](#七、消息过滤:Tag 与 SQL92)
-
- [7.1 Tag 过滤(常用)](#7.1 Tag 过滤(常用))
- [7.2 SQL92 过滤(灵活)](#7.2 SQL92 过滤(灵活))
- 八、总结
-
- [8.1 一张图回顾全文](#8.1 一张图回顾全文)
- [8.2 核心要点浓缩(十二条)](#8.2 核心要点浓缩(十二条))
📖 术语速查表(每个词都用人话解释)
** 消息类型类**
| 术语 | 一句话白话解释 |
|---|---|
| 顺序消息 | 同一业务 Key 的消息按发送顺序消费------靠"同一 Key 路由到同一 Queue + 单 Queue 顺序消费"保证 |
| 全局顺序 | 整个 Topic 所有消息严格有序------只用 1 个 Queue,牺牲全部并行度(极少用) |
| 分区顺序 | 同一业务 Key 内有序(如同一订单的 create→pay→cancel)------多 Queue 并行,Key 哈希路由(常用) |
| 延迟消息 | 消息发送后延迟指定时间才可消费------RocketMQ 4.x 支持 18 个固定延迟等级(1s/5s/10s/.../2h),5.x 支持任意时间 |
| 事务消息 | 保证"本地事务 + 消息发送"的最终一致------半消息+回查机制 |
| 半消息(Half Message) | 事务消息的"预备状态"------消息已发到 Broker 但对 Consumer 不可见,等本地事务提交后才"确认" |
| 消息回查 | Broker 定时检查半消息对应的本地事务状态------Producer 没主动确认时,Broker 主动回查 |
| 批量消息 | 多条消息打包一次发送------减少网络往返(同一 Topic、总大小 <4MB) |
** 消费与可靠性类**
| 术语 | 一句话白话解释 |
|---|---|
| 集群消费 | 同组 Consumer 分摊消费 Queue------一条消息只被组内一个 Consumer 消费(最常用) |
| 广播消费 | 同组每个 Consumer 都收到每条消息------适合配置下发/缓存刷新 |
| Push / Pull | Push 模式 Broker"推"给 Consumer(本质是长轮询 Pull),Pull 模式 Consumer 自己拉(可控速率) |
| 长轮询 | Consumer 拉消息没数据时 Broker hold 住请求(默认 15s) ,有新消息立即返回------兼具 Push 的实时性和 Pull 的可控性 |
| 消费重试 | 消费失败自动重试------重试间隔递增(10s/30s/1min...),超过 16 次进死信队列 |
| 死信队列(DLQ) | 重试 16 次仍失败的消息进死信队列(%DLQ%消费组)------人工介入处理,不丢消息 |
| 消费幂等 | 同一条消息消费多次结果一致------靠业务唯一键去重(消息可能重复投递) |
| Tag 过滤 | Consumer 订阅时指定 Tag(`subscribe("topic", "tagA |
| SQL92 过滤 | 按消息属性写 SQL 过滤("amount > 100 AND region = 'cn'")------比 Tag 灵活,Broker 要开 enablePropertyFilter |
一、顺序消息:两保证缺一不可
1.1 需求与分类
需求:订单的 create → pay → cancel 必须按顺序消费(不能先消费 cancel 再消费 create)。
| 类型 | 含义 | 实现 | 代价 |
|---|---|---|---|
| 全局顺序 | 整个 Topic 严格有序 | 只用 1 个 Queue | 无并行度,吞吐极低(极少用) |
| 分区顺序(常用) | 同一业务 Key 内有序 | Key 哈希路由到固定 Queue | Queue 内串行消费 |
1.2 实现原理:发送有序 + 消费有序
#mermaid-svg-U63BMMJ42yMRq1kT{font-family:Microsoft YaHei;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-U63BMMJ42yMRq1kT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-U63BMMJ42yMRq1kT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-U63BMMJ42yMRq1kT .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-U63BMMJ42yMRq1kT .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-U63BMMJ42yMRq1kT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-U63BMMJ42yMRq1kT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-U63BMMJ42yMRq1kT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-U63BMMJ42yMRq1kT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-U63BMMJ42yMRq1kT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-U63BMMJ42yMRq1kT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-U63BMMJ42yMRq1kT .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-U63BMMJ42yMRq1kT .marker.cross{stroke:#0b0b0b;}#mermaid-svg-U63BMMJ42yMRq1kT svg{font-family:Microsoft YaHei;font-size:16px;}#mermaid-svg-U63BMMJ42yMRq1kT p{margin:0;}#mermaid-svg-U63BMMJ42yMRq1kT .label{font-family:Microsoft YaHei;color:#333;}#mermaid-svg-U63BMMJ42yMRq1kT .cluster-label text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-U63BMMJ42yMRq1kT .cluster-label span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-U63BMMJ42yMRq1kT .cluster-label span p{background-color:transparent;}#mermaid-svg-U63BMMJ42yMRq1kT .label text,#mermaid-svg-U63BMMJ42yMRq1kT span{fill:#333;color:#333;}#mermaid-svg-U63BMMJ42yMRq1kT .node rect,#mermaid-svg-U63BMMJ42yMRq1kT .node circle,#mermaid-svg-U63BMMJ42yMRq1kT .node ellipse,#mermaid-svg-U63BMMJ42yMRq1kT .node polygon,#mermaid-svg-U63BMMJ42yMRq1kT .node path{fill:#fff4dd;stroke:hsl(40.5882352941, 60%, 83.3333333333%);stroke-width:1px;}#mermaid-svg-U63BMMJ42yMRq1kT .rough-node .label text,#mermaid-svg-U63BMMJ42yMRq1kT .node .label text,#mermaid-svg-U63BMMJ42yMRq1kT .image-shape .label,#mermaid-svg-U63BMMJ42yMRq1kT .icon-shape .label{text-anchor:middle;}#mermaid-svg-U63BMMJ42yMRq1kT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-U63BMMJ42yMRq1kT .rough-node .label,#mermaid-svg-U63BMMJ42yMRq1kT .node .label,#mermaid-svg-U63BMMJ42yMRq1kT .image-shape .label,#mermaid-svg-U63BMMJ42yMRq1kT .icon-shape .label{text-align:center;}#mermaid-svg-U63BMMJ42yMRq1kT .node.clickable{cursor:pointer;}#mermaid-svg-U63BMMJ42yMRq1kT .root .anchor path{fill:#0b0b0b!important;stroke-width:0;stroke:#0b0b0b;}#mermaid-svg-U63BMMJ42yMRq1kT .arrowheadPath{fill:#0b0b0b;}#mermaid-svg-U63BMMJ42yMRq1kT .edgePath .path{stroke:#0b0b0b;stroke-width:2.0px;}#mermaid-svg-U63BMMJ42yMRq1kT .flowchart-link{stroke:#0b0b0b;fill:none;}#mermaid-svg-U63BMMJ42yMRq1kT .edgeLabel{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-U63BMMJ42yMRq1kT .edgeLabel p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-U63BMMJ42yMRq1kT .edgeLabel rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-U63BMMJ42yMRq1kT .labelBkg{background-color:rgba(243.9999999999, 220.9999999998, 255, 0.5);}#mermaid-svg-U63BMMJ42yMRq1kT .cluster rect{fill:hsl(220.5882352941, 100%, 98.3333333333%);stroke:hsl(220.5882352941, 60%, 88.3333333333%);stroke-width:1px;}#mermaid-svg-U63BMMJ42yMRq1kT .cluster text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-U63BMMJ42yMRq1kT .cluster span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-U63BMMJ42yMRq1kT div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:Microsoft YaHei;font-size:12px;background:hsl(220.5882352941, 100%, 98.3333333333%);border:1px solid hsl(220.5882352941, 60%, 88.3333333333%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-U63BMMJ42yMRq1kT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-U63BMMJ42yMRq1kT rect.text{fill:none;stroke-width:0;}#mermaid-svg-U63BMMJ42yMRq1kT .icon-shape,#mermaid-svg-U63BMMJ42yMRq1kT .image-shape{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-U63BMMJ42yMRq1kT .icon-shape p,#mermaid-svg-U63BMMJ42yMRq1kT .image-shape p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);padding:2px;}#mermaid-svg-U63BMMJ42yMRq1kT .icon-shape .label rect,#mermaid-svg-U63BMMJ42yMRq1kT .image-shape .label rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-U63BMMJ42yMRq1kT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-U63BMMJ42yMRq1kT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-U63BMMJ42yMRq1kT :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 顺序消息两保证。
① 发送侧:
同一业务 Key 路由到同一 Queue。
MessageQueueSelector
按 orderId 哈希选 Queue。
→ 同一订单的消息进同一 Queue。
② 消费侧:
单 Queue 顺序消费。
MessageListenerOrderly,
一个 Queue 同一时刻
只被一个线程消费。
→ Queue 内消息按序消费。
效果:
同一订单的消息,
发送有序 + 消费有序 = 端到端有序。
为什么两个保证缺一不可:
- 只保证发送有序(同 Key 同 Queue),但消费端用并发监听器(MessageListenerConcurrently)------多线程同时消费一个 Queue 的消息,顺序被打乱;
- 只保证消费有序(Orderly),但发送端轮询选 Queue------同一订单的消息散在多个 Queue,Queue 之间没有顺序。
1.3 代码示例(本项目订单场景)
java
// 发送侧:按 orderId 哈希选 Queue(同一订单进同一 Queue)
rocketMQTemplate.syncSendOrderly("order-topic", payload, String.valueOf(orderId));
// 底层:MessageQueueSelector 按 orderId.hashCode() % queueNum 选 Queue
// 消费侧:顺序监听器(单 Queue 单线程串行消费)
@Component
@RocketMQMessageListener(
topic = "order-topic",
consumerGroup = "order-status-group",
consumeMode = ConsumeMode.ORDERLY // 关键:顺序消费
)
public class OrderStatusConsumer implements RocketMQListener<OrderStatusMsg> {
@Override
public void onMessage(OrderStatusMsg msg) {
// 同一订单的 create → pay → cancel 严格按序到达这里
orderService.updateStatus(msg);
}
}
1.4 顺序消息的两个坑
| 坑 | 说明 | 应对 |
|---|---|---|
| 消费失败阻塞 Queue | 顺序消费失败不能跳过(跳过=乱序)------本地重试(默认 Integer.MAX_VALUE 次),一条卡住全 Queue 卡住 | 设置 maxReconsumeTimes,超限进死信+告警,人工处理 |
| Broker 挂/Queue 数变化 | 哈希路由的目标 Queue 变了------新旧消息进不同 Queue,顺序被破坏 | 发送失败重试时不换 Queue(retryTimesWhenSendFailed=0 或业务容忍短暂乱序+版本号兜底) |
注意 :顺序消息牺牲并行度(同一 Queue 串行消费)------只有需要顺序的业务 Key 用顺序消息,其他用普通消息。能用"状态机+版本号"在消费端兜底的(如订单状态只能单向流转),优先普通消息+幂等状态机,比顺序消息吞吐高得多。
二、延迟消息:18 个等级与内部 Topic
2.1 需求与等级
需求:订单 30 分钟未支付自动取消------发一条"30 分钟后消费"的延迟消息。
RocketMQ 4.x 支持 18 个固定延迟等级:
text
level: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
delay: 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
5.x 改进 :支持任意时间延迟(基于时间轮 TimerWheel + TimerLog),不再限于 18 级。
2.2 实现原理:内部 Topic + 定时转发
#mermaid-svg-zDEhYcXrR0mEAnp9{font-family:Microsoft YaHei;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zDEhYcXrR0mEAnp9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zDEhYcXrR0mEAnp9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zDEhYcXrR0mEAnp9 .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-zDEhYcXrR0mEAnp9 .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-zDEhYcXrR0mEAnp9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zDEhYcXrR0mEAnp9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zDEhYcXrR0mEAnp9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zDEhYcXrR0mEAnp9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zDEhYcXrR0mEAnp9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zDEhYcXrR0mEAnp9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zDEhYcXrR0mEAnp9 .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-zDEhYcXrR0mEAnp9 .marker.cross{stroke:#0b0b0b;}#mermaid-svg-zDEhYcXrR0mEAnp9 svg{font-family:Microsoft YaHei;font-size:16px;}#mermaid-svg-zDEhYcXrR0mEAnp9 p{margin:0;}#mermaid-svg-zDEhYcXrR0mEAnp9 .label{font-family:Microsoft YaHei;color:#333;}#mermaid-svg-zDEhYcXrR0mEAnp9 .cluster-label text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-zDEhYcXrR0mEAnp9 .cluster-label span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-zDEhYcXrR0mEAnp9 .cluster-label span p{background-color:transparent;}#mermaid-svg-zDEhYcXrR0mEAnp9 .label text,#mermaid-svg-zDEhYcXrR0mEAnp9 span{fill:#333;color:#333;}#mermaid-svg-zDEhYcXrR0mEAnp9 .node rect,#mermaid-svg-zDEhYcXrR0mEAnp9 .node circle,#mermaid-svg-zDEhYcXrR0mEAnp9 .node ellipse,#mermaid-svg-zDEhYcXrR0mEAnp9 .node polygon,#mermaid-svg-zDEhYcXrR0mEAnp9 .node path{fill:#fff4dd;stroke:hsl(40.5882352941, 60%, 83.3333333333%);stroke-width:1px;}#mermaid-svg-zDEhYcXrR0mEAnp9 .rough-node .label text,#mermaid-svg-zDEhYcXrR0mEAnp9 .node .label text,#mermaid-svg-zDEhYcXrR0mEAnp9 .image-shape .label,#mermaid-svg-zDEhYcXrR0mEAnp9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-zDEhYcXrR0mEAnp9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zDEhYcXrR0mEAnp9 .rough-node .label,#mermaid-svg-zDEhYcXrR0mEAnp9 .node .label,#mermaid-svg-zDEhYcXrR0mEAnp9 .image-shape .label,#mermaid-svg-zDEhYcXrR0mEAnp9 .icon-shape .label{text-align:center;}#mermaid-svg-zDEhYcXrR0mEAnp9 .node.clickable{cursor:pointer;}#mermaid-svg-zDEhYcXrR0mEAnp9 .root .anchor path{fill:#0b0b0b!important;stroke-width:0;stroke:#0b0b0b;}#mermaid-svg-zDEhYcXrR0mEAnp9 .arrowheadPath{fill:#0b0b0b;}#mermaid-svg-zDEhYcXrR0mEAnp9 .edgePath .path{stroke:#0b0b0b;stroke-width:2.0px;}#mermaid-svg-zDEhYcXrR0mEAnp9 .flowchart-link{stroke:#0b0b0b;fill:none;}#mermaid-svg-zDEhYcXrR0mEAnp9 .edgeLabel{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-zDEhYcXrR0mEAnp9 .edgeLabel p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-zDEhYcXrR0mEAnp9 .edgeLabel rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-zDEhYcXrR0mEAnp9 .labelBkg{background-color:rgba(243.9999999999, 220.9999999998, 255, 0.5);}#mermaid-svg-zDEhYcXrR0mEAnp9 .cluster rect{fill:hsl(220.5882352941, 100%, 98.3333333333%);stroke:hsl(220.5882352941, 60%, 88.3333333333%);stroke-width:1px;}#mermaid-svg-zDEhYcXrR0mEAnp9 .cluster text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-zDEhYcXrR0mEAnp9 .cluster span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-zDEhYcXrR0mEAnp9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:Microsoft YaHei;font-size:12px;background:hsl(220.5882352941, 100%, 98.3333333333%);border:1px solid hsl(220.5882352941, 60%, 88.3333333333%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zDEhYcXrR0mEAnp9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zDEhYcXrR0mEAnp9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-zDEhYcXrR0mEAnp9 .icon-shape,#mermaid-svg-zDEhYcXrR0mEAnp9 .image-shape{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-zDEhYcXrR0mEAnp9 .icon-shape p,#mermaid-svg-zDEhYcXrR0mEAnp9 .image-shape p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);padding:2px;}#mermaid-svg-zDEhYcXrR0mEAnp9 .icon-shape .label rect,#mermaid-svg-zDEhYcXrR0mEAnp9 .image-shape .label rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-zDEhYcXrR0mEAnp9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zDEhYcXrR0mEAnp9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zDEhYcXrR0mEAnp9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
下一秒再扫
Producer 发延迟消息。
setDelayTimeLevel(16)。
Broker 偷梁换柱。
Topic → SCHEDULE_TOPIC_XXXX。
Queue = 15(level 16 - 1)。
真实 Topic 存进消息属性。
ScheduleMessageService 每秒扫一次。
遍历 18 个 Queue,
检查每个 Queue 的队头。
队头到期?
到期 →
恢复真实 Topic,
写入 CommitLog。
Consumer 可见。
未到期 →
什么都不做。
等下一秒再扫。
为什么队头检查就够 :同一等级的消息延迟时间相同 → 先进先到期 → 队列天然有序 → 只看队头(和 RabbitMQ 队列级 TTL 的"队头阻塞"是同一个思想,但 RocketMQ 按等级分队列规避了不同延迟混排的问题)。
2.3 代码示例(本项目订单超时取消)
java
// 发送:30 分钟后检查订单(level 16 = 30min)
rocketMQTemplate.syncSend("order-timeout-topic",
MessageBuilder.withPayload(new OrderTimeoutMsg(orderId)).build(),
3000, // 发送超时 3s
16); // 延迟等级 16 = 30 分钟
// 消费:二次校验(延迟消息到期 ≠ 订单一定未支付)
@Override
public void onMessage(OrderTimeoutMsg msg) {
Order order = orderService.getById(msg.getOrderId());
if (order == null || order.getStatus() != OrderStatus.UNPAID) {
return; // 已支付/已取消 → 幂等跳过
}
orderService.cancelOrder(msg.getOrderId(), "超时未支付");
}
局限与近似方案 :4.x 只支持固定等级,不支持任意时间(如"延迟 45s")------任意延迟 = 选最近等级(1m)+ 消费时二次校验时间(没到点就再发一条更短延迟的消息"接力")。
与定时任务扫表对比:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 延迟消息 | 精准触发、无扫表压力、分布式天然支持 | 依赖 MQ 特性 |
| 定时任务扫表 | 实现简单 | 扫表有延迟+数据库压力,百万级订单扫不动 |
三、事务消息:半消息 + 回查
3.1 需求:本地事务与消息发送的原子性
订单库 insert 订单 + 发"扣库存"消息------要么都成功,要么都不生效。直接"先写库再发消息"或"先发消息再写库"都有一致性窗口(25-A 篇详解),事务消息就是把这个窗口交给 MQ 内核解决。
3.2 流程:半消息 + 本地事务 + 回查
Consumer(库存服务) Broker Producer(订单服务) Consumer(库存服务) Broker Producer(订单服务) #mermaid-svg-QWISPSfyjGBpFGdN{font-family:Microsoft YaHei;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QWISPSfyjGBpFGdN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QWISPSfyjGBpFGdN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QWISPSfyjGBpFGdN .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-QWISPSfyjGBpFGdN .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-QWISPSfyjGBpFGdN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QWISPSfyjGBpFGdN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QWISPSfyjGBpFGdN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QWISPSfyjGBpFGdN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QWISPSfyjGBpFGdN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QWISPSfyjGBpFGdN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QWISPSfyjGBpFGdN .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-QWISPSfyjGBpFGdN .marker.cross{stroke:#0b0b0b;}#mermaid-svg-QWISPSfyjGBpFGdN svg{font-family:Microsoft YaHei;font-size:16px;}#mermaid-svg-QWISPSfyjGBpFGdN p{margin:0;}#mermaid-svg-QWISPSfyjGBpFGdN .actor{stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:#fff4dd;}#mermaid-svg-QWISPSfyjGBpFGdN text.actor>tspan{fill:#333;stroke:none;}#mermaid-svg-QWISPSfyjGBpFGdN .actor-line{stroke:hsl(40.5882352941, 60%, 83.3333333333%);}#mermaid-svg-QWISPSfyjGBpFGdN .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-QWISPSfyjGBpFGdN .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-QWISPSfyjGBpFGdN .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-QWISPSfyjGBpFGdN #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-QWISPSfyjGBpFGdN .sequenceNumber{fill:#f4f4f4;}#mermaid-svg-QWISPSfyjGBpFGdN #sequencenumber{fill:#333;}#mermaid-svg-QWISPSfyjGBpFGdN #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-QWISPSfyjGBpFGdN .messageText{fill:#333;stroke:none;}#mermaid-svg-QWISPSfyjGBpFGdN .labelBox{stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:#fff4dd;}#mermaid-svg-QWISPSfyjGBpFGdN .labelText,#mermaid-svg-QWISPSfyjGBpFGdN .labelText>tspan{fill:#333;stroke:none;}#mermaid-svg-QWISPSfyjGBpFGdN .loopText,#mermaid-svg-QWISPSfyjGBpFGdN .loopText>tspan{fill:#333;stroke:none;}#mermaid-svg-QWISPSfyjGBpFGdN .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:hsl(40.5882352941, 60%, 83.3333333333%);}#mermaid-svg-QWISPSfyjGBpFGdN .note{stroke:hsl(52.6829268293, 60%, 73.9215686275%);fill:#fff5ad;}#mermaid-svg-QWISPSfyjGBpFGdN .noteText,#mermaid-svg-QWISPSfyjGBpFGdN .noteText>tspan{fill:#333;stroke:none;}#mermaid-svg-QWISPSfyjGBpFGdN .activation0{fill:hsl(-79.4117647059, 100%, 93.3333333333%);stroke:hsl(-79.4117647059, 100%, 83.3333333333%);}#mermaid-svg-QWISPSfyjGBpFGdN .activation1{fill:hsl(-79.4117647059, 100%, 93.3333333333%);stroke:hsl(-79.4117647059, 100%, 83.3333333333%);}#mermaid-svg-QWISPSfyjGBpFGdN .activation2{fill:hsl(-79.4117647059, 100%, 93.3333333333%);stroke:hsl(-79.4117647059, 100%, 83.3333333333%);}#mermaid-svg-QWISPSfyjGBpFGdN .actorPopupMenu{position:absolute;}#mermaid-svg-QWISPSfyjGBpFGdN .actorPopupMenuPanel{position:absolute;fill:#fff4dd;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-QWISPSfyjGBpFGdN .actor-man line{stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:#fff4dd;}#mermaid-svg-QWISPSfyjGBpFGdN .actor-man circle,#mermaid-svg-QWISPSfyjGBpFGdN line{stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:#fff4dd;stroke-width:2px;}#mermaid-svg-QWISPSfyjGBpFGdN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 半消息存入 RMQ_SYS_TRANS_HALF_TOPIC 对 Consumer 不可见 loop 定时回查(默认 60s 一次,最多 15 次) 返回 UNKNOWN 继续等下次回查 超过 15 次默认回滚 alt 本地事务成功 本地事务失败 Producer 宕机 · 没确认 发送半消息(Half Message) 1 半消息发送成功 2 执行本地事务(insert 订单表) 3 Commit 4 半消息重写到真实 Topic 5 拉取消息 6 投递(执行扣库存,消费幂等) 7 Rollback 8 半消息丢弃(写 op 记录标记已处理) 9 CheckLocalTransaction(带业务 Key) 10 查库判断本地事务状态 11 COMMIT / ROLLBACK / UNKNOWN 12
3.3 半消息的"偷梁换柱"
半消息不是存在真实 Topic 里,而是存在内部 Topic RMQ_SYS_TRANS_HALF_TOPIC ------Consumer 订阅不到;Commit 时把消息重新写入真实 Topic (对 Consumer 可见),同时写一条 op 记录到 RMQ_SYS_TRANS_OP_HALF_TOPIC 标记"已处理";回查任务扫 Half Topic 与 Op Topic 的差集,没被标记的就是"悬而未决"的半消息。
3.4 代码示例(支付成功 → 加积分)
java
// 发送事务消息(corp-ceo-order 支付回调)
@RocketMQTransactionListener
public class PayTransactionListener implements RocketMQLocalTransactionListener {
@Autowired
private OrderService orderService;
// 半消息发送成功后回调:执行本地事务
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
String orderId = msg.getHeaders().get(RocketMQHeaders.KEYS, String.class);
orderService.markPaid(orderId); // 本地事务:订单标记已支付
return RocketMQLocalTransactionState.COMMIT; // 消息对 Consumer 可见
} catch (Exception e) {
return RocketMQLocalTransactionState.ROLLBACK; // 消息丢弃
}
}
// Broker 回查:Producer 没确认时,Broker 定时来问"本地事务成了没"
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
String orderId = msg.getHeaders().get(RocketMQHeaders.KEYS, String.class);
Order order = orderService.getById(orderId);
if (order == null) {
return RocketMQLocalTransactionState.ROLLBACK;
}
return order.isPaid()
? RocketMQLocalTransactionState.COMMIT
: RocketMQLocalTransactionState.UNKNOWN; // 还没执行完,下次再查
}
}
// 发送
rocketMQTemplate.sendMessageInTransaction("point-grant-topic",
MessageBuilder.withPayload(pointMsg)
.setHeader(RocketMQHeaders.KEYS, orderId) // 业务 Key:回查时定位本地事务
.build(), null);
3.5 三个关键点
- 半消息对 Consumer 不可见------存在内部 Topic(RMQ_SYS_TRANS_HALF_TOPIC),确认后才转发到真实 Topic;
- 回查是兜底 ------Producer 宕机没确认时,Broker 定时回查本地事务状态(默认间隔 60s,最多 15 次,超限回滚)------回查逻辑必须能"查库判断事务状态",所以业务 Key 要放进消息 Header;
- 消费端仍要幂等------事务消息保证"本地事务+发送"一致,但消费可能重复(本章第六节)。
与本地消息表对比:
| 维度 | 事务消息 | 本地消息表 |
|---|---|---|
| 消息表维护 | MQ 内核(半消息) | 自己建表+定时任务投递 |
| 回查/补偿 | Broker 自动回查 | 定时任务扫表重发 |
| 依赖 | RocketMQ 特性(Kafka/RabbitMQ 没有) | 只依赖 DB,MQ 无关 |
| 侵入 | 低(一个 Listener) | 中(消息表+扫表任务) |
一句话 :事务消息把"消息表+投递"交给 MQ 内核(半消息+回查),本地消息表自己维护------事务消息少一套定时任务,但绑定 RocketMQ。
四、消费模型:集群与广播
4.1 两种消费模式
| 模式 | 行为 | 位点存储 | 适用 |
|---|---|---|---|
| 集群消费(默认) | 同组 Consumer 分摊 Queue------一条消息只被组内一个 Consumer 消费 | Broker | 业务处理(订单/库存/积分) |
| 广播消费 | 同组每个 Consumer 都收到每条消息 | Consumer 本地 | 配置下发/缓存刷新/通知 |
#mermaid-svg-C0o5XAJwe7a5LK5T{font-family:Microsoft YaHei;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-C0o5XAJwe7a5LK5T .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-C0o5XAJwe7a5LK5T .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-C0o5XAJwe7a5LK5T .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-C0o5XAJwe7a5LK5T .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-C0o5XAJwe7a5LK5T .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-C0o5XAJwe7a5LK5T .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-C0o5XAJwe7a5LK5T .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-C0o5XAJwe7a5LK5T .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-C0o5XAJwe7a5LK5T .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-C0o5XAJwe7a5LK5T .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-C0o5XAJwe7a5LK5T .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-C0o5XAJwe7a5LK5T .marker.cross{stroke:#0b0b0b;}#mermaid-svg-C0o5XAJwe7a5LK5T svg{font-family:Microsoft YaHei;font-size:16px;}#mermaid-svg-C0o5XAJwe7a5LK5T p{margin:0;}#mermaid-svg-C0o5XAJwe7a5LK5T .label{font-family:Microsoft YaHei;color:#333;}#mermaid-svg-C0o5XAJwe7a5LK5T .cluster-label text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-C0o5XAJwe7a5LK5T .cluster-label span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-C0o5XAJwe7a5LK5T .cluster-label span p{background-color:transparent;}#mermaid-svg-C0o5XAJwe7a5LK5T .label text,#mermaid-svg-C0o5XAJwe7a5LK5T span{fill:#333;color:#333;}#mermaid-svg-C0o5XAJwe7a5LK5T .node rect,#mermaid-svg-C0o5XAJwe7a5LK5T .node circle,#mermaid-svg-C0o5XAJwe7a5LK5T .node ellipse,#mermaid-svg-C0o5XAJwe7a5LK5T .node polygon,#mermaid-svg-C0o5XAJwe7a5LK5T .node path{fill:#fff4dd;stroke:hsl(40.5882352941, 60%, 83.3333333333%);stroke-width:1px;}#mermaid-svg-C0o5XAJwe7a5LK5T .rough-node .label text,#mermaid-svg-C0o5XAJwe7a5LK5T .node .label text,#mermaid-svg-C0o5XAJwe7a5LK5T .image-shape .label,#mermaid-svg-C0o5XAJwe7a5LK5T .icon-shape .label{text-anchor:middle;}#mermaid-svg-C0o5XAJwe7a5LK5T .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-C0o5XAJwe7a5LK5T .rough-node .label,#mermaid-svg-C0o5XAJwe7a5LK5T .node .label,#mermaid-svg-C0o5XAJwe7a5LK5T .image-shape .label,#mermaid-svg-C0o5XAJwe7a5LK5T .icon-shape .label{text-align:center;}#mermaid-svg-C0o5XAJwe7a5LK5T .node.clickable{cursor:pointer;}#mermaid-svg-C0o5XAJwe7a5LK5T .root .anchor path{fill:#0b0b0b!important;stroke-width:0;stroke:#0b0b0b;}#mermaid-svg-C0o5XAJwe7a5LK5T .arrowheadPath{fill:#0b0b0b;}#mermaid-svg-C0o5XAJwe7a5LK5T .edgePath .path{stroke:#0b0b0b;stroke-width:2.0px;}#mermaid-svg-C0o5XAJwe7a5LK5T .flowchart-link{stroke:#0b0b0b;fill:none;}#mermaid-svg-C0o5XAJwe7a5LK5T .edgeLabel{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-C0o5XAJwe7a5LK5T .edgeLabel p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-C0o5XAJwe7a5LK5T .edgeLabel rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-C0o5XAJwe7a5LK5T .labelBkg{background-color:rgba(243.9999999999, 220.9999999998, 255, 0.5);}#mermaid-svg-C0o5XAJwe7a5LK5T .cluster rect{fill:hsl(220.5882352941, 100%, 98.3333333333%);stroke:hsl(220.5882352941, 60%, 88.3333333333%);stroke-width:1px;}#mermaid-svg-C0o5XAJwe7a5LK5T .cluster text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-C0o5XAJwe7a5LK5T .cluster span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-C0o5XAJwe7a5LK5T div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:Microsoft YaHei;font-size:12px;background:hsl(220.5882352941, 100%, 98.3333333333%);border:1px solid hsl(220.5882352941, 60%, 88.3333333333%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-C0o5XAJwe7a5LK5T .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-C0o5XAJwe7a5LK5T rect.text{fill:none;stroke-width:0;}#mermaid-svg-C0o5XAJwe7a5LK5T .icon-shape,#mermaid-svg-C0o5XAJwe7a5LK5T .image-shape{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-C0o5XAJwe7a5LK5T .icon-shape p,#mermaid-svg-C0o5XAJwe7a5LK5T .image-shape p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);padding:2px;}#mermaid-svg-C0o5XAJwe7a5LK5T .icon-shape .label rect,#mermaid-svg-C0o5XAJwe7a5LK5T .image-shape .label rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-C0o5XAJwe7a5LK5T .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-C0o5XAJwe7a5LK5T .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-C0o5XAJwe7a5LK5T :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Topic: 4 个 Queue
集群消费 Group A
C1 消费 Q0/Q1,C2 消费 Q2/Q3
一条消息只被组内一个 Consumer 处理。
广播消费 Group B
C3、C4 都收到全部 4 个 Queue 的消息
每台机器都刷新本地缓存。
Group A 与 Group B 互不影响
同一条消息被每个组各消费一次。
4.2 Push 的本质:长轮询
| 模式 | 行为 | 特点 |
|---|---|---|
| Push(常用) | Broker hold 住 Consumer 的拉取请求(默认 15s),有新消息立即返回 | 实时性接近推模式,流控权在 Consumer(不会被推爆) |
| Pull | Consumer 自己控制拉取频率 | 完全可控,但要自己处理"拉太快/太慢" |
| Lite Pull(4.6+) | 简化版 Pull,自动位点管理 | 流处理场景 |
一句话 :RocketMQ 的 Push 是"伪装成推的拉"------长轮询兼具 Push 的实时性和 Pull 的可控性,Consumer 永远不会被 Broker 推爆。
4.3 并发消费 vs 顺序消费
| 维度 | Concurrently(默认) | Orderly |
|---|---|---|
| 线程模型 | 线程池并发消费一个 Queue 的多条消息 | 一个 Queue 同一时刻只有一个线程消费(加锁) |
| 失败处理 | 发回 Broker 延迟重试(不阻塞后续) | 本地重试(阻塞 Queue) |
| 吞吐 | 高 | 低(串行) |
| 适用 | 绝大多数业务 | 状态流转等强顺序场景 |
五、消费重试与死信队列
5.1 重试机制
#mermaid-svg-nnyxaGxUZdO45JyL{font-family:Microsoft YaHei;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-nnyxaGxUZdO45JyL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-nnyxaGxUZdO45JyL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-nnyxaGxUZdO45JyL .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-nnyxaGxUZdO45JyL .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-nnyxaGxUZdO45JyL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-nnyxaGxUZdO45JyL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nnyxaGxUZdO45JyL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nnyxaGxUZdO45JyL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-nnyxaGxUZdO45JyL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nnyxaGxUZdO45JyL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nnyxaGxUZdO45JyL .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-nnyxaGxUZdO45JyL .marker.cross{stroke:#0b0b0b;}#mermaid-svg-nnyxaGxUZdO45JyL svg{font-family:Microsoft YaHei;font-size:16px;}#mermaid-svg-nnyxaGxUZdO45JyL p{margin:0;}#mermaid-svg-nnyxaGxUZdO45JyL .label{font-family:Microsoft YaHei;color:#333;}#mermaid-svg-nnyxaGxUZdO45JyL .cluster-label text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-nnyxaGxUZdO45JyL .cluster-label span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-nnyxaGxUZdO45JyL .cluster-label span p{background-color:transparent;}#mermaid-svg-nnyxaGxUZdO45JyL .label text,#mermaid-svg-nnyxaGxUZdO45JyL span{fill:#333;color:#333;}#mermaid-svg-nnyxaGxUZdO45JyL .node rect,#mermaid-svg-nnyxaGxUZdO45JyL .node circle,#mermaid-svg-nnyxaGxUZdO45JyL .node ellipse,#mermaid-svg-nnyxaGxUZdO45JyL .node polygon,#mermaid-svg-nnyxaGxUZdO45JyL .node path{fill:#fff4dd;stroke:hsl(40.5882352941, 60%, 83.3333333333%);stroke-width:1px;}#mermaid-svg-nnyxaGxUZdO45JyL .rough-node .label text,#mermaid-svg-nnyxaGxUZdO45JyL .node .label text,#mermaid-svg-nnyxaGxUZdO45JyL .image-shape .label,#mermaid-svg-nnyxaGxUZdO45JyL .icon-shape .label{text-anchor:middle;}#mermaid-svg-nnyxaGxUZdO45JyL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-nnyxaGxUZdO45JyL .rough-node .label,#mermaid-svg-nnyxaGxUZdO45JyL .node .label,#mermaid-svg-nnyxaGxUZdO45JyL .image-shape .label,#mermaid-svg-nnyxaGxUZdO45JyL .icon-shape .label{text-align:center;}#mermaid-svg-nnyxaGxUZdO45JyL .node.clickable{cursor:pointer;}#mermaid-svg-nnyxaGxUZdO45JyL .root .anchor path{fill:#0b0b0b!important;stroke-width:0;stroke:#0b0b0b;}#mermaid-svg-nnyxaGxUZdO45JyL .arrowheadPath{fill:#0b0b0b;}#mermaid-svg-nnyxaGxUZdO45JyL .edgePath .path{stroke:#0b0b0b;stroke-width:2.0px;}#mermaid-svg-nnyxaGxUZdO45JyL .flowchart-link{stroke:#0b0b0b;fill:none;}#mermaid-svg-nnyxaGxUZdO45JyL .edgeLabel{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-nnyxaGxUZdO45JyL .edgeLabel p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-nnyxaGxUZdO45JyL .edgeLabel rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-nnyxaGxUZdO45JyL .labelBkg{background-color:rgba(243.9999999999, 220.9999999998, 255, 0.5);}#mermaid-svg-nnyxaGxUZdO45JyL .cluster rect{fill:hsl(220.5882352941, 100%, 98.3333333333%);stroke:hsl(220.5882352941, 60%, 88.3333333333%);stroke-width:1px;}#mermaid-svg-nnyxaGxUZdO45JyL .cluster text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-nnyxaGxUZdO45JyL .cluster span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-nnyxaGxUZdO45JyL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:Microsoft YaHei;font-size:12px;background:hsl(220.5882352941, 100%, 98.3333333333%);border:1px solid hsl(220.5882352941, 60%, 88.3333333333%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-nnyxaGxUZdO45JyL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-nnyxaGxUZdO45JyL rect.text{fill:none;stroke-width:0;}#mermaid-svg-nnyxaGxUZdO45JyL .icon-shape,#mermaid-svg-nnyxaGxUZdO45JyL .image-shape{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-nnyxaGxUZdO45JyL .icon-shape p,#mermaid-svg-nnyxaGxUZdO45JyL .image-shape p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);padding:2px;}#mermaid-svg-nnyxaGxUZdO45JyL .icon-shape .label rect,#mermaid-svg-nnyxaGxUZdO45JyL .image-shape .label rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-nnyxaGxUZdO45JyL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-nnyxaGxUZdO45JyL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-nnyxaGxUZdO45JyL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
消费失败,
返回 RECONSUME_LATER。
进入重试 Topic。
%RETRY%ConsumerGroup。
重试间隔递增。
10s / 30s / 1min / 2min......
共 16 次。
16 次后
仍失败?
是 → 进死信队列 DLQ。
%DLQ%ConsumerGroup。
人工介入处理。
否 → 继续重试。
重试间隔 = 延迟消息机制的复用 :第 N 次重试的延迟 = 延迟等级 N+2(10s 起步)------重试 Topic 本质是"发给自己的延迟消息"(第二节原理直接复用)。
5.2 重试次数与间隔对照
| 重试次数 | 1 | 2 | 3 | 4 | 5 | 6 | ... | 16 |
|---|---|---|---|---|---|---|---|---|
| 间隔 | 10s | 30s | 1min | 2min | 3min | 4min | ... | 2h |
16 次重试总时长 ≈ 4 小时 46 分 ------超过后进 %DLQ%消费组 死信队列。死信队列默认保留 3 天,要配监控告警+人工处理流程(03 篇)。
5.3 死信处理的最佳实践
text
① 监控:%DLQ% 开头的 Topic 有新消息 → P1 告警(死信 = 业务处理不了的消息)
② 排查:控制台按 msgId 查死信内容 + 消费异常堆栈
③ 处理:修复 bug 后,控制台"重新发送"死信消息到原 Topic(或写工具批量重放)
④ 兜底:无法自动处理的人工修数,处理结果记录工单
六、消费幂等:MQ 使用的第一军规
6.1 为什么消息一定会重复
| 重复来源 | 场景 |
|---|---|
| 发送重试 | Producer 发送成功但网络超时没收到 ACK → 重发 → Broker 存了两条 |
| 消费重试 | Consumer 处理成功但 ACK 前宕机 → Broker 认为没消费 → 重新投递 |
| Rebalance | Queue 重新分配,位点回退 → 部分消息重复消费 |
| 人工重放 | 死信重发/故障恢复后手动重置位点 |
结论 :RocketMQ 只承诺 At Least Once(至少一次),Exactly-Once 要靠消费端幂等实现------"消息可能重复"不是 bug,是设计。
6.2 幂等三板斧
java
// ① 状态机幂等:状态只能单向流转,重复消息因状态不符被跳过(本项目 OrderTimeoutConsumer)
Order order = orderService.getById(msg.getOrderId());
if (order == null || order.getStatus() != OrderStatus.UNPAID) {
return; // 已支付/已取消 → 幂等跳过
}
// ② 唯一键幂等:业务唯一键做唯一索引,重复插入直接失败(本项目 InventoryDeductConsumer)
// orderId+skuId 唯一索引,tryDeduct 内部 INSERT ... ON DUPLICATE 判断
boolean firstTime = inventoryService.tryDeduct(msg);
if (!firstTime) {
log.warn("重复扣减消息,忽略: {}", msg.getOrderId());
}
// ③ 去重表/Redis 幂等:消费前 SETNX msgId(或业务 Key),已存在则跳过
Boolean first = redisTemplate.opsForValue()
.setIfAbsent("mq:dedup:" + msg.getKeys(), "1", Duration.ofDays(1));
if (Boolean.FALSE.equals(first)) {
return; // 重复消息
}
三板斧的选择:
| 方案 | 适用 | 注意 |
|---|---|---|
| 状态机 | 有状态流转的业务(订单/支付) | 首选------业务语义天然幂等 |
| 唯一键 | 插入类操作(扣减流水/积分流水) | 唯一索引是最后防线 |
| 去重表/Redis | 无状态操作(发短信/推送) | Redis 去重要处理"SETNX 成功但业务失败"(删 Key 或短 TTL 重试) |
注意 :不能用 msgId 做幂等键 ------发送重试产生的两条消息 msgId 不同!要用业务 Key(订单号+操作类型)。
七、消息过滤:Tag 与 SQL92
7.1 Tag 过滤(常用)
java
// 发送:同一 Topic 用 Tag 细分
rocketMQTemplate.syncSend("order-topic:pay", payMsg); // Tag=pay
rocketMQTemplate.syncSend("order-topic:cancel", cancelMsg); // Tag=cancel
// 消费:只订阅感兴趣的 Tag
@RocketMQMessageListener(topic = "order-topic", selectorExpression = "pay || cancel", ...)
两级过滤 :Broker 端用 ConsumeQueue 里的 tag hashcode 粗过滤 (跳过不匹配的,省网络),Consumer 端再按 tag 字符串精确过滤(防哈希冲突)------这就是 ConsumeQueue 条目里存 tag hashcode 的原因(01 篇 3.2)。
7.2 SQL92 过滤(灵活)
java
// 发送:消息带用户属性
MessageBuilder.withPayload(msg)
.setHeader("amount", 500).setHeader("region", "cn").build();
// 消费:按属性 SQL 过滤(Broker 要开 enablePropertyFilter=true)
@RocketMQMessageListener(topic = "order-topic",
selectorExpression = "amount > 100 AND region = 'cn'",
selectorType = SelectorType.SQL92, ...)
选择 :Tag 够用就用 Tag(性能好、简单);多维度组合过滤才上 SQL92(Broker 开销大)。
八、总结
8.1 一张图回顾全文
#mermaid-svg-H0UgZfSGEKYeEBhD{font-family:Microsoft YaHei;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-H0UgZfSGEKYeEBhD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-H0UgZfSGEKYeEBhD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-H0UgZfSGEKYeEBhD .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-H0UgZfSGEKYeEBhD .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-H0UgZfSGEKYeEBhD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-H0UgZfSGEKYeEBhD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-H0UgZfSGEKYeEBhD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-H0UgZfSGEKYeEBhD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-H0UgZfSGEKYeEBhD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-H0UgZfSGEKYeEBhD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-H0UgZfSGEKYeEBhD .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-H0UgZfSGEKYeEBhD .marker.cross{stroke:#0b0b0b;}#mermaid-svg-H0UgZfSGEKYeEBhD svg{font-family:Microsoft YaHei;font-size:16px;}#mermaid-svg-H0UgZfSGEKYeEBhD p{margin:0;}#mermaid-svg-H0UgZfSGEKYeEBhD .label{font-family:Microsoft YaHei;color:#333;}#mermaid-svg-H0UgZfSGEKYeEBhD .cluster-label text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-H0UgZfSGEKYeEBhD .cluster-label span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-H0UgZfSGEKYeEBhD .cluster-label span p{background-color:transparent;}#mermaid-svg-H0UgZfSGEKYeEBhD .label text,#mermaid-svg-H0UgZfSGEKYeEBhD span{fill:#333;color:#333;}#mermaid-svg-H0UgZfSGEKYeEBhD .node rect,#mermaid-svg-H0UgZfSGEKYeEBhD .node circle,#mermaid-svg-H0UgZfSGEKYeEBhD .node ellipse,#mermaid-svg-H0UgZfSGEKYeEBhD .node polygon,#mermaid-svg-H0UgZfSGEKYeEBhD .node path{fill:#fff4dd;stroke:hsl(40.5882352941, 60%, 83.3333333333%);stroke-width:1px;}#mermaid-svg-H0UgZfSGEKYeEBhD .rough-node .label text,#mermaid-svg-H0UgZfSGEKYeEBhD .node .label text,#mermaid-svg-H0UgZfSGEKYeEBhD .image-shape .label,#mermaid-svg-H0UgZfSGEKYeEBhD .icon-shape .label{text-anchor:middle;}#mermaid-svg-H0UgZfSGEKYeEBhD .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-H0UgZfSGEKYeEBhD .rough-node .label,#mermaid-svg-H0UgZfSGEKYeEBhD .node .label,#mermaid-svg-H0UgZfSGEKYeEBhD .image-shape .label,#mermaid-svg-H0UgZfSGEKYeEBhD .icon-shape .label{text-align:center;}#mermaid-svg-H0UgZfSGEKYeEBhD .node.clickable{cursor:pointer;}#mermaid-svg-H0UgZfSGEKYeEBhD .root .anchor path{fill:#0b0b0b!important;stroke-width:0;stroke:#0b0b0b;}#mermaid-svg-H0UgZfSGEKYeEBhD .arrowheadPath{fill:#0b0b0b;}#mermaid-svg-H0UgZfSGEKYeEBhD .edgePath .path{stroke:#0b0b0b;stroke-width:2.0px;}#mermaid-svg-H0UgZfSGEKYeEBhD .flowchart-link{stroke:#0b0b0b;fill:none;}#mermaid-svg-H0UgZfSGEKYeEBhD .edgeLabel{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-H0UgZfSGEKYeEBhD .edgeLabel p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-H0UgZfSGEKYeEBhD .edgeLabel rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-H0UgZfSGEKYeEBhD .labelBkg{background-color:rgba(243.9999999999, 220.9999999998, 255, 0.5);}#mermaid-svg-H0UgZfSGEKYeEBhD .cluster rect{fill:hsl(220.5882352941, 100%, 98.3333333333%);stroke:hsl(220.5882352941, 60%, 88.3333333333%);stroke-width:1px;}#mermaid-svg-H0UgZfSGEKYeEBhD .cluster text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-H0UgZfSGEKYeEBhD .cluster span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-H0UgZfSGEKYeEBhD div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:Microsoft YaHei;font-size:12px;background:hsl(220.5882352941, 100%, 98.3333333333%);border:1px solid hsl(220.5882352941, 60%, 88.3333333333%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-H0UgZfSGEKYeEBhD .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-H0UgZfSGEKYeEBhD rect.text{fill:none;stroke-width:0;}#mermaid-svg-H0UgZfSGEKYeEBhD .icon-shape,#mermaid-svg-H0UgZfSGEKYeEBhD .image-shape{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-H0UgZfSGEKYeEBhD .icon-shape p,#mermaid-svg-H0UgZfSGEKYeEBhD .image-shape p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);padding:2px;}#mermaid-svg-H0UgZfSGEKYeEBhD .icon-shape .label rect,#mermaid-svg-H0UgZfSGEKYeEBhD .image-shape .label rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-H0UgZfSGEKYeEBhD .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-H0UgZfSGEKYeEBhD .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-H0UgZfSGEKYeEBhD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} RocketMQ 消息特性与消费。
三种特色消息。
顺序:同 Key 同 Queue
- 单 Queue 顺序消费,
两保证缺一不可。
延迟:18 等级,
内部 Topic 定时转发。
事务:半消息 + 回查,最终一致。
消费模型。
集群分摊(位点存 Broker)。
广播全收(位点存本地)。
Push 本质是长轮询。
并发消费 vs 顺序消费。
重试与死信。
失败进 %RETRY% Topic。
间隔递增重试 16 次
(≈ 4h46m)。
超限进 %DLQ% 死信,
人工处理。
幂等与过滤。
At Least Once 是承诺,
幂等是义务。
三板斧:状态机 / 唯一键 / 去重表。
幂等键用业务 Key,不用 msgId。
Tag 两级过滤,SQL92 灵活。
8.2 核心要点浓缩(十二条)
- 顺序消息两保证 :同一业务 Key 路由到同一 Queue(MessageQueueSelector)+ 单 Queue 顺序消费(MessageListenerOrderly)------缺一不可,牺牲并行度换顺序。
- 全局顺序 vs 分区顺序 :全局顺序只用 1 个 Queue(吞吐极低),分区顺序按 Key 哈希(常用)------能用"状态机+版本号"兜底的优先普通消息。
- 顺序消费的坑 :消费失败本地重试阻塞整个 Queue------设 maxReconsumeTimes 超限进死信+告警。
- 延迟消息原理 :Broker 把消息偷梁换柱到 SCHEDULE_TOPIC_XXXX(每等级一个 Queue),定时任务扫队头到期转发回真实 Topic------同等级先进先到期,队头检查就够。
- 延迟消息局限:4.x 只有 18 个固定等级,任意延迟 = 最近等级 + 消费二次校验(或 5.x 任意时间延迟)。
- 事务消息流程 :半消息(Consumer 不可见)→ 本地事务 → Commit/Rollback → 回查兜底(默认 60s 间隔,最多 15 次)------保证"本地事务+消息发送"最终一致。
- 半消息存储 :存在 RMQ_SYS_TRANS_HALF_TOPIC,Commit 时重写真实 Topic,Op Topic 标记已处理------回查扫两个 Topic 的差集。
- 回查的前提 :业务 Key 放消息 Header------回查逻辑要能"查库判断本地事务状态"。
- 集群 vs 广播 :集群消费分摊 Queue(位点存 Broker),广播消费全收(位点存本地)------Push 本质是长轮询,兼具实时性和流控权。
- 重试与死信 :失败进 %RETRY% Topic,间隔递增重试 16 次(总时长≈4h46m,复用延迟消息机制),超限进 %DLQ%------死信要监控告警+人工处理。
- 消费幂等是第一军规 :RocketMQ 只承诺 At Least Once------幂等三板斧:状态机(首选)/唯一键/去重表;幂等键用业务 Key 不用 msgId(发送重试的 msgId 不同)。
- 消息过滤 :Tag 两级过滤(Broker 按 hashcode 粗过滤 + Consumer 精确过滤),SQL92 按属性灵活过滤------Tag 够用就用 Tag。
📌 最后一句话 :RocketMQ 的三种特色消息都是"在 CommitLog 存储之上做的文章 "------顺序消息靠"路由到同一 Queue",延迟消息靠"内部 Topic 定时转发",事务消息靠"半消息+回查",重试机制靠"延迟消息复用"。理解了存储模型(01 篇),这些特性就都是顺理成章的组合。而消费幂等是所有特性的公共前提------MQ 只保证"至少一次","恰好一次"永远是消费端的责任。
📌 配套阅读:
上一篇:《06-01-A-RocketMQ架构与存储原理详解.md》
如果这篇文章对你有帮助,欢迎点赞、收藏、关注!🚀