Appearance
RocketMQ:交易型消息中间件指南
RocketMQ 是 Apache 顶级项目,最早来自阿里巴巴大规模电商交易场景。
相比“通用队列”,RocketMQ 更突出的是业务消息语义:普通消息、顺序消息、延时消息、事务消息、消费重试与死信。
一句话理解
RocketMQ 是一个面向业务系统的分布式消息中间件:NameServer 负责路由发现,Broker 负责消息存储和投递,Producer/Consumer 通过 Topic 和 Queue 完成发布订阅。
适合与不适合
适合:
- 订单、支付、库存等核心交易链路。
- 需要事务消息保证最终一致。
- 需要顺序消息处理状态流转。
- 需要延时消息处理超时取消、到期提醒。
不适合:
- 纯日志/埋点/数据湖事件流,Kafka 生态更成熟。
- 简单后台任务队列,RabbitMQ 更轻。
- 多租户平台化和存储计算分离诉求特别强,Pulsar 更合适。
核心组件
| 组件 | 作用 |
|---|---|
| NameServer | 路由注册中心,保存 Broker 与 Topic 路由 |
| Broker | 消息存储、投递、查询、复制 |
| Topic | 消息逻辑分类 |
| MessageQueue | Topic 的物理队列,并发和顺序的基本单位 |
| Producer | 消息生产者 |
| Consumer | 消息消费者 |
| Consumer Group | 消费者组,同组共同消费 |
| Proxy | 5.x 引入的重要接入层能力,便于协议与云原生接入 |
Topic 与 Queue
一个 Topic 通常包含多个 Queue,Queue 分布在不同 Broker 上。
设计原则:
- Topic 按业务域和事件类型规划,例如
order-event-topic、payment-event-topic、coupon-command-topic。不要把所有业务事件塞进一个大 Topic,否则权限、监控、重试和排障都会混在一起。 - Queue 数量决定消费并发上限。Queue 太少会限制 Consumer Group 的并行度,Queue 太多会增加调度、拉取和路由管理成本。
- 有顺序要求的消息要按同一业务 key 路由到同一 Queue,例如同一个
orderId的创建、支付、发货、完成事件。 - Topic 名称和消息类型要稳定。普通消息、顺序消息、延时消息、事务消息的行为不同,不建议在同一个 Topic 内随意混用。
工程上可以把 Topic 看成“事件契约的容器”,把 Queue 看成“并发与顺序的执行单元”。规划 Topic 时先问业务边界,规划 Queue 时再问吞吐和顺序。
消息契约设计
RocketMQ 能保证消息可靠投递,但不能替业务定义消息含义。真正影响后续维护的是消息契约。
一条订单事件至少应包含:
| 字段 | 说明 |
|---|---|
eventId | 事件唯一标识,用于链路追踪和排重 |
eventType | 事件类型,例如 order.created、order.paid |
eventVersion | 契约版本,例如 v1 |
businessKey | 业务主键,例如 orderId,用于顺序、幂等和排查 |
occurredAt | 业务事件发生时间 |
traceId | 请求链路标识 |
payload | 业务载荷,只放消费者需要的字段 |
消息体不要直接序列化数据库对象。数据库表字段会随实现变化,消息契约面向系统协作,应该保持兼容演进。新增字段尽量向后兼容,删除字段要走版本升级,不要让下游消费者在运行时才发现字段消失。
四类核心消息
| 类型 | 能力 | 场景 |
|---|---|---|
| Normal Message | 普通消息 | 异步通知、系统解耦 |
| FIFO Message | 顺序消息 | 订单状态流转、交易撮合 |
| Delay Message | 延时/定时投递 | 超时关闭订单、到期提醒 |
| Transaction Message | 事务消息 | 本地事务与消息发送最终一致 |
普通消息流程
普通消息适合大多数事件通知,例如订单创建后通知库存、搜索、积分、消息中心。它的重点是可靠传输,不额外承诺顺序、延时或本地事务一致性。
生产端要关注三件事:
- 发送失败要明确处理。网络异常、Broker 重启、请求超时都可能触发重试,但重试不能替代业务最终一致设计。
- 消息要设置业务 key。排查“这笔订单的消息去哪了”时,
orderId比内部msgId更有用。 - 发送结果只代表 Broker 接收成功,不代表消费者已经处理成功。
消费端要接受“至少一次”的现实:消息可能重复投递,消费者必须幂等。
顺序消息
RocketMQ 顺序消息的关键是:同一业务序列进入同一个 MessageQueue,并由消费者顺序处理。
适合:
- 订单状态:创建 -> 支付 -> 发货 -> 完成。
- 账户流水。
- 数据同步中的同一主键变更。
注意:
- 全局顺序通常不推荐,因为会牺牲并发。
- 分区顺序是更常见做法。
- 顺序消息失败时容易阻塞同一个 Queue 后续消息。消费者逻辑要尽量短,不能在顺序消费者里做长时间外部调用。
- 顺序不等于状态一定正确。订单状态仍要由数据库状态机兜底,例如已支付订单不能被超时关闭。
适合顺序消息的判断标准是:同一个业务 key 的多个事件必须按先后关系处理,且可以接受该 key 维度的串行执行。只要业务能通过状态机、幂等和最终一致兜住,就不必强行上顺序消息。
延时消息
延时消息是“现在发送,将来投递”。常见场景:
- 30 分钟未支付关闭订单。
- 会议开始前 10 分钟提醒。
- 优惠券到期提醒。
延时消息适合“到时间后检查”的场景,不适合表达“到时间后一定执行某个结论”。订单超时关闭就是典型例子:延时消息到期后只能触发检查,消费者仍要查询订单库。如果订单已支付或已取消,就应该忽略这条超时检查消息。
设计延时消息时要明确:
- 延时消息是否允许重复触发。
- 到期后执行的是“检查”还是“直接变更”。
- 业务状态以哪个系统为准。
- 如果延时消息丢失或延迟过长,是否有定时补偿任务兜底。
对于强业务场景,延时消息通常还要配合数据库状态、补偿扫描和监控告警,不能成为唯一控制点。
事务消息
事务消息用于解决“本地事务成功,但消息没发出去”或“消息发出去了,本地事务失败”的一致性问题。
关键点:
- 事务消息不是强一致分布式事务,而是最终一致方案。
- 本地事务状态回查必须可靠、可重入。
- Consumer 仍然需要幂等。
- 事务消息解决的是生产端本地事务与消息发送的一致性,不负责保证下游消费者一定成功处理。
- 回查逻辑不能依赖内存变量。Broker 回查时,Producer 可能已经重启,必须能通过数据库订单状态、支付流水状态等持久化数据判断提交还是回滚。
- 回查接口要可重入。重复回查同一笔业务时,应返回同一个结论,不能因为中间状态变化导致消息悬挂。
支付成功发券可以使用事务消息:支付服务先发送半消息,再提交本地支付事务;本地事务成功后提交消息,下游发券消费者收到 order.paid 后幂等发券。如果提交状态丢失,Broker 回查支付流水,确认支付成功就提交消息,确认失败就回滚消息。
消费重试与死信
实践建议:
- 可重试错误:网络超时、临时下游不可用。
- 不可重试错误:消息格式错误、业务状态非法。
- 重试次数达到上限后进入死信队列,并触发告警。
消费失败要先分类,再决定返回失败还是吞掉并记录。所有错误都抛出给 RocketMQ 重试,会把不可恢复错误变成重复压力。
| 错误类型 | 例子 | 建议处理 |
|---|---|---|
| 临时错误 | 下游接口超时、数据库连接短暂异常 | 返回失败,让 RocketMQ 按策略重试 |
| 资源限流 | 第三方接口限流、线程池满 | 本地限流 + 返回失败,必要时降低消费并发 |
| 业务等待 | 订单状态暂未同步、支付回调还未到 | 可以短暂重试,但要设置最大等待和补偿任务 |
| 数据错误 | 消息缺少必填字段、金额格式非法 | 记录错误,进入异常表或死信,不要无限重试 |
| 幂等冲突 | 优惠券已发、状态已处理 | 识别为成功,返回消费成功 |
死信队列不是垃圾桶,而是人工和系统补偿入口。生产环境至少要有死信告警、死信查看、死信原因记录和重放策略。重放前要先修复根因,否则只是把同一批毒丸消息重新打回主链路。
消费者幂等
RocketMQ 无法替业务消除所有重复消息。重复可能来自生产端重试、Broker 投递重试、消费者超时、网络抖动或人工重放。消费者要把“同一条业务消息处理多次”和“处理一次”的结果做成一致。
常见幂等方式:
| 方式 | 适用场景 | 说明 |
|---|---|---|
| 唯一键约束 | 发券、积分、记账 | 用 orderId + couponTemplateId、eventId 建唯一索引 |
| 状态机约束 | 订单状态流转 | 只允许 待支付 -> 已支付,拒绝非法回退 |
| 幂等记录表 | 外部接口调用、跨库处理 | 消费前检查处理记录,成功后落库 |
| 乐观锁 | 库存扣减、账户变更 | 通过版本号或状态条件更新 |
| 业务去重缓存 | 短时间重复请求 | 只能做辅助,不能替代持久化幂等 |
发券消费者的典型做法是:以 orderId + couponTemplateId 建唯一约束,插入成功表示首次发券,唯一键冲突表示已经发过,消费者应返回成功而不是继续重试。
统一案例:订单超时关闭 + 支付成功发券
Topic 规划
| Topic | 消息类型 | 说明 |
|---|---|---|
order-topic | 普通/顺序 | 订单状态事件 |
order-timeout-topic | 延时消息 | 超时检查 |
payment-topic | 事务消息 | 支付成功事件 |
coupon-topic | 普通消息 | 发券任务 |
流程图
关键设计
- 下单成功后发送 30 分钟延时消息。
- 延时消息到期后只做状态检查,不直接假设订单未支付。
- 支付成功使用事务消息,保证支付本地事务和
order.paid事件最终一致。 - 发券消费者以
orderId + couponTemplateId幂等。 - 同一订单状态事件可按
orderId做分区顺序。
高可用与部署
| 模式 | 说明 | 适用 |
|---|---|---|
| 单 Master | 简单但有单点风险 | 本地/测试 |
| 多 Master | 性能高,Master 宕机期间部分消息不可消费 | 对可用性要求一般 |
| 多 Master 多 Slave 异步复制 | 性能与可用性平衡 | 常见生产场景 |
| 多 Master 多 Slave 同步双写 | 可靠性更高,延迟略高 | 核心交易链路 |
监控指标
| 指标 | 含义 |
|---|---|
| Topic 消息堆积 | 消费能力是否不足 |
| Consumer TPS | 消费吞吐 |
| Send TPS | 发送吞吐 |
| 消费失败率 | 下游或业务异常 |
| Broker 磁盘使用率 | 存储压力 |
| CommitLog 刷盘延迟 | 可靠性与性能风险 |
| DLQ 数量 | 是否存在毒丸消息 |
常见坑
| 问题 | 现象 | 原因 | 解决方案 |
|---|---|---|---|
| 把事务消息当强一致事务 | 本地支付成功了,但下游发券仍可能失败;团队误以为事务消息能覆盖所有下游 | 事务消息只保证生产端本地事务与消息发送的一致性,下游消费仍是异步最终一致 | 下游消费者必须幂等;失败进入重试和死信;关键业务增加补偿任务和对账任务 |
| 事务回查依赖内存状态 | Producer 重启后无法判断半消息该提交还是回滚,半消息长期悬挂 | 回查逻辑没有以数据库业务状态为准 | 回查接口只读持久化状态,例如支付流水、订单状态;同一事务 ID 重复回查必须返回稳定结果 |
| 顺序消息滥用全局顺序 | 消费吞吐很低,一个慢消息拖住整个 Topic | 把“同订单有序”误写成“所有订单全局有序” | 按 orderId、accountId 等业务 key 做分区顺序;只在同一 key 内保证顺序 |
| 顺序消费者做重外部调用 | 某个订单处理慢,后续同 Queue 消息堆积明显 | 顺序消费要求同 Queue 串行,慢调用会扩大阻塞 | 顺序消费者只做短逻辑;外部慢操作拆成异步任务;失败要快速返回并进入受控重试 |
| 消费失败不分类 | 毒丸消息反复重试,消费失败率和堆积持续上升 | 消息格式错误、业务非法状态也被当作临时错误重试 | 按错误类型处理:临时错误重试,数据错误记录异常并进入死信,幂等冲突返回成功 |
| 死信队列无人处理 | DLQ 数量增长,但业务缺失一直没人发现 | 只配置了死信,没有告警、查看和重放流程 | 对 DLQ 数量和增长率告警;记录失败原因;修复根因后再定向重放 |
| Consumer 不幂等 | 重复发券、重复扣库存、重复写账 | RocketMQ 是至少一次投递语义,重复消息不可完全避免 | 用业务唯一键、状态机、幂等表或乐观锁兜底;重复处理应返回消费成功 |
| 不设置业务 key | 线上排查时只能看到内部 msgId,很难定位某笔订单消息 | 消息缺少稳定业务标识 | 发送消息时设置 orderId、paymentId 等业务 key;日志里同时打印 key、Topic、Consumer Group、traceId |
| 延时消息直接改状态 | 订单已支付后仍被超时关闭 | 把“到期检查”误写成“到期必然关闭” | 延时消息只触发检查;消费者查询订单库并用状态机判断;已支付或已取消时直接忽略 |
| Topic 规划过粗 | 多业务共用一个 Topic,权限、堆积、重试互相影响 | 为了省事把所有事件放到一个主题 | 按业务域和事件类型拆分 Topic;高风险、高吞吐、不同重试策略的消息分开治理 |
| 只看 TPS 不看堆积 | 发送和消费 TPS 看起来正常,但延迟越来越高 | 消费速率略低于生产速率,堆积持续增长 | 同时看 Topic 堆积、消费延迟、失败率、重试量、DLQ;告警应关注趋势 |
| 消息体过大 | Broker 网络、磁盘和客户端内存压力异常 | 把文件、长文本、大对象直接塞进 MQ | MQ 只传业务事件和文件引用;大文件放对象存储,消息里放 URL、版本和校验信息 |
总结
RocketMQ 更适合承载带业务语义的消息链路:普通消息用于解耦,顺序消息用于同 key 状态流转,延时消息用于到期检查,事务消息用于生产端本地事务与消息发送最终一致。
工程落地时不要把能力理解成魔法开关。顺序要靠 key 设计,事务要靠可靠回查,延时要靠状态检查,可靠消费要靠重试、死信、幂等和补偿共同完成。
