RocketMQ 第 21 章:面试常见题目合并版
将 30 个 RocketMQ 高频题按关联关系合并为架构模型、发送消费、可靠性、事务、延迟、存储、高可用、积压调优和选型对比等主题簇,减少重复并形成更适合面试复盘的回答顺序。
第 21 章:RocketMQ 面试常见题目合并版
原始 30 题里有不少内容互相重叠:比如“不重复消费”和“幂等性”本质是一组题,“消息堆积”和“系统调优”应该放在同一条排障链路里,“存储机制、持久化、日志写入性能”也应该合并回答。本章把这些题目按面试思考路径重排为 13 个主题簇。
阅读方式:先看
21.0的合并映射,知道原题被放到哪里;再按定位选型 -> 领域模型 -> 发送 -> 消费 -> 可靠性 -> 存储高可用 -> 排障调优的顺序复习。
21.0 原 30 题合并映射
| 新主题簇 | 合并的原题 | 为什么合并 | 对应主讲章节 |
|---|---|---|---|
| 21.1 定位、场景与选型 | 1、28 | “RocketMQ 是什么”和“RocketMQ vs Kafka”本质都是选型题。 | 第 1 章:消息队列基础与技术定位、第 19 章:业务架构设计与选型 |
| 21.2 领域模型与服务发现 | 2、3、20 | Producer、Consumer、Topic、Tag、MessageQueue、NameServer 是一组基础模型。 | 第 2 章:整体架构与领域模型、第 12 章:Topic 与资源治理、第 17 章:4.x 到 5.x 架构演进 |
| 21.3 发送链路与批量发送 | 4、26,以及 2 的 Producer 部分 | 同步、异步、单向、批量、队列选择都属于 Producer 发送链路。 | 第 4 章:Producer 发送链路、第 14 章:性能优化与容量规划 |
| 21.4 消费模型、负载均衡与位点 | 5、8、14、18、19 | Push/Simple/Pull、集群/广播、Rebalance、Offset、并发/顺序消费经常一起追问。 | 第 5 章:Consumer 与 ACK、第 6 章:Rebalance、位点与积压、第 17 章:5.x 消息级负载均衡 |
| 21.5 过滤、Topic 设计与资源治理 | 3、9 | Topic/Tag 设计与 Tag/SQL92 过滤应该一起讲。 | 第 12 章:Topic、Tag、Key、SQL92 与资源治理 |
| 21.6 顺序消息与乱序治理 | 16、27,以及 19 的顺序消费部分 | 顺序保证和乱序处理是一正一反。 | 第 9 章:FIFO 顺序消息、第 6 章:Rebalance 与重复边界 |
| 21.7 可靠投递、重试、DLQ 与幂等 | 6、10、12、21、30 | RocketMQ 的可靠性题最终都会落到 at-least-once、重试、死信和业务幂等。 | 第 8 章:端到端可靠性、重试与幂等、第 4 章:Producer 可靠发送、第 5 章:Consumer ACK 与重试 |
| 21.8 事务消息与最终一致性 | 13 | 事务消息是独立高频题,但要放在可靠性之后。 | 第 11 章:事务消息与最终一致性、第 19 章:业务一致性架构落地 |
| 21.9 延迟消息与定时任务 | 7 | 延迟消息有 4.x / 5.x 差异,单独成簇更清楚。 | 第 10 章:延迟消息、定时消息与任务调度 |
| 21.10 存储、持久化与写入性能 | 15、23、24 | CommitLog、ConsumeQueue、刷盘、PageCache、mmap 是一条存储链路。 | 第 7 章:存储引擎、第 14 章:性能优化与容量规划 |
| 21.11 主从、高可用与故障切换 | 17、22 | 主从架构、复制、Controller、DLedger 都是 HA 题。 | 第 13 章:高可用与故障切换、第 17 章:Controller 与架构演进、第 16 章:灾备与跨集群 |
| 21.12 消息轨迹、积压与系统调优 | 11、25、29 | 轨迹用于定位,积压和调优是同一套排障方法论。 | 第 15 章:可观测性与 Runbook、第 6 章:消息积压、第 14 章:性能与容量 |
| 21.13 面试总口诀 | 原最后总结 | 用一段话收束全章。 | 第 20 章:资深面试题库与追问链 |
21.1 RocketMQ 的定位、应用场景与选型
一句话回答
RocketMQ 是一个分布式消息中间件和事件流平台,核心价值是把系统之间的同步强依赖改造成异步事件驱动,同时提供可靠投递、顺序消息、事务消息、延迟消息、过滤、重试、死信、轨迹和高可用能力。
RocketMQ 官方领域模型里,消息生命周期包含生产、存储、消费三个阶段;Topic 由多个 MessageQueue 组成,ConsumerGroup 管理消费者、过滤、重试和消费位点等订阅状态。(rocketmq.apache.org)
常见应用场景
| 场景 | 解决的问题 | 典型例子 |
|---|---|---|
| 异步解耦 | 上游不用同步等待多个下游 | 下单后异步通知库存、积分、短信、数据分析 |
| 削峰填谷 | 把瞬时流量缓冲到 Broker | 秒杀请求先进入 MQ,消费者按固定速率落库 |
| 最终一致性 | 让本地事务和跨服务事件形成闭环 | 订单创建成功后异步扣库存、发优惠券 |
| 顺序事件处理 | 同一业务 key 的事件按顺序处理 | 订单状态流转、账户流水、binlog 同步 |
| 延迟任务 | 到期后再触发业务动作 | 下单 30 分钟未支付自动关闭 |
| 数据同步与事件驱动 | 多个系统独立订阅业务事件 | 业务系统 -> RocketMQ -> Flink / ES / ClickHouse |
RocketMQ 与 Kafka 怎么选
不要简单回答“谁更强”。面试里更好的说法是按业务目标选型:
| 维度 | RocketMQ | Kafka |
|---|---|---|
| 典型定位 | 业务消息中间件、事件驱动、交易链路 | 日志流、数据管道、流处理生态 |
| 业务能力 | 事务消息、延迟消息、顺序消息、Tag/SQL 过滤、重试死信、消息轨迹更贴近业务系统 | 高吞吐、分区日志、Kafka Streams、Connect 生态强 |
| 消费模型 | ConsumerGroup + MessageQueue,5.x 进一步强化消息级负载均衡 | ConsumerGroup + Partition |
| 适合场景 | 订单、支付、交易、通知、任务调度、业务最终一致性 | 日志采集、指标流水、数据湖、CDC、流计算平台 |
面试模板:
如果是交易型业务消息,关注事务消息、延迟消息、重试死信、消息轨迹和业务幂等,我会优先考虑 RocketMQ;如果是日志和数据流平台,关注超高吞吐、流处理生态和数据管道,我会优先考虑 Kafka。真正选型要看一致性边界、吞吐、延迟、运维经验、生态组件和团队熟悉度。
21.2 领域模型、核心组件与服务发现
对应主讲章节:第 2 章:整体架构、核心组件与领域模型、第 12 章:Topic、Tag、Key、SQL92、MessageQueue 与资源治理、第 17 章:从 4.x 到 5.x 架构演进。
核心对象关系
Producer -> NameServer 查询路由
Producer -> Broker 写入消息
Broker -> CommitLog / ConsumeQueue / IndexFile
Consumer -> NameServer 查询路由
Consumer -> Broker 拉取或接收消息
Consumer -> ACK / 提交消费结果
| 对象 | 作用 | 面试要点 |
|---|---|---|
| Producer | 生产消息并发送到 Broker | 不固定依赖某个 Broker,会基于 Topic 路由选择 MessageQueue |
| Broker | 存储、投递、查询消息 | 核心数据节点,负责 CommitLog、ConsumeQueue、IndexFile、重试和 DLQ |
| Topic | 业务维度的消息容器 | 粗粒度业务域隔离,下面有多个 MessageQueue |
| MessageQueue | Topic 下的分片 | 类似 Kafka partition,是并发、顺序、位点管理的重要边界 |
| ConsumerGroup | 一组共享订阅行为和消费进度的消费者 | 同组分摊消息,不同组独立消费 |
| NameServer | 轻量级路由注册中心 | 保存 Broker、Topic、Queue 路由,不存业务消息 |
| Proxy | 5.x 统一接入层 | 支持 gRPC、多语言 SDK、接入转发,通常不持久化业务消息 |
| Controller | 5.x 自动主从切换控制面 | 管理副本、选主、故障切换,不参与普通消息读写 |
NameServer 的作用
NameServer 是 RocketMQ 的路由发现组件,保存 Broker 集群路由、Topic 路由和队列信息。Producer 和 Consumer 启动后会连接 NameServer 获取路由,然后再与 Broker 建立连接。(rocketmq.apache.org)
服务发现流程:
Broker 启动
-> 向所有 NameServer 注册
-> 定期发送心跳
Producer / Consumer 启动
-> 连接 NameServer
-> 获取 Topic 路由
-> 连接目标 Broker
-> 定期刷新路由
NameServer 可以相对轻量,是因为它主要保存路由元数据,不保存业务消息,也不负责强一致协调。多个 NameServer 节点之间通常不做复杂同步,客户端同时配置多个地址,一个不可用时可以访问另一个。
Topic 与 Tag 怎么设计
Topic 是业务域和存储容器,Tag 是同一个 Topic 内的轻量分类。官方概念里,Topic 是传输和存储同一业务逻辑消息的顶层容器,MessageTag 是 Topic 下更细粒度的消息分类属性。(rocketmq.apache.org)
合理设计:
Topic: OrderTopic
Tag: OrderCreated
Tag: OrderPaid
Tag: OrderCancelled
Tag: OrderRefunded
不建议:
OrderCreatedTopic
OrderPaidTopic
OrderCancelledTopic
这样 Topic 太细,会导致路由元数据、权限、监控、治理成本上升。
也不建议:
Topic: AllBusinessTopic
Tag: OrderPaid
Tag: UserRegistered
Tag: PaymentFailed
这样 Topic 太粗,不同业务的权限、堆积、限流、重试、SLA 会互相影响。
面试模板:
Topic 用来做业务域、存储、权限、SLA 和容量隔离;Tag 用来做同一业务域内的轻量分类和 Broker 端过滤。Topic 不宜过多,也不宜过粗,通常按业务域、消息类型、可靠性等级和流量特征来拆。
21.3 发送链路、发送方式与批量消息
对应主讲章节:第 4 章:Producer 发送模型、路由选择、重试机制与底层发送链路、第 14 章:性能优化、流控、压测与容量规划。
Producer 发送主链路
Producer 的核心流程:
1. 启动 Producer
2. 连接 NameServer
3. 获取 Topic 路由
4. 根据负载均衡或业务 key 选择 MessageQueue
5. 向对应 Broker 发送消息
6. Broker 写入 CommitLog
7. Broker 返回 SendResult
8. Producer 根据结果判断成功、失败或重试
普通消息通常轮询发送到不同 MessageQueue;顺序消息需要按业务 key 固定路由,例如同一个 orderId 始终进入同一个队列或同一个 MessageGroup。
同步、异步、单向发送
RocketMQ 4.x 文档列出同步、异步、单向三种发送方式。同步和异步能得到服务端响应,属于可靠发送;单向发送没有响应和重试,不适合不能丢的核心业务。(rocketmq.apache.org)
| 方式 | 是否等待 Broker 响应 | 可靠性 | 性能 | 场景 |
|---|---|---|---|---|
| 同步发送 | 等待 SendResult | 高 | 中 | 订单、支付、账户变更 |
| 异步发送 | 不阻塞主线程,通过 callback 收结果 | 高 | 高 | 高吞吐业务事件、视频任务通知 |
| 单向发送 | 不等待、不回调 | 低 | 最高 | 日志、监控、可丢弃埋点 |
同步发送:
SendResult result = producer.send(msg);
异步发送:
producer.send(msg, new SendCallback() {
public void onSuccess(SendResult sendResult) {
// 发送成功
}
public void onException(Throwable e) {
// 失败补偿
}
});
单向发送:
producer.sendOneway(msg);
面试模板:
同步发送可靠但 RT 高;异步发送吞吐更好,但 callback 必须处理失败,应用不能过早 shutdown producer;单向发送最快但没有确认,只适合日志和监控这类允许丢失的场景。核心业务如果发送失败,应结合本地表或 Outbox 补偿。
批量消息
批量消息本质是把多条消息合并成一次网络请求,减少 API 调用次数和网络开销,从而提升吞吐。RocketMQ 批量消息文档也说明,把消息成批发送可以提高吞吐并减少 API 与网络调用。(rocketmq.apache.org)
适合批量的场景:
- 日志采集;
- 用户行为事件;
- 批量通知;
- 数据同步;
- 非严格顺序的高吞吐写入。
优化原则:
| 原则 | 原因 |
|---|---|
| 控制批次大小 | 批次过大可能导致单条请求超限、RT 变高、失败重试成本变大 |
| 批次内尽量同 Topic、同业务类型 | 便于路由、过滤和治理 |
| 消费端也要批量处理 | 发送批量但消费单条落库,瓶颈仍在下游 |
| 失败重试要幂等 | 批量中部分成功、部分失败时更容易产生重复 |
| 顺序消息谨慎批量 | 批次内顺序和业务 key 边界要清楚 |
21.4 消费模型、负载均衡与消费位点
对应主讲章节:第 5 章:Consumer 类型、长轮询、POP、ACK 与完整消费链路、第 6 章:Rebalance、消费位点、负载均衡与消息积压、第 17 章:从 4.x 到 5.x 架构演进。
三类 Consumer API
RocketMQ 5.x 明确区分 PushConsumer、SimpleConsumer、PullConsumer。PushConsumer 更封装,SimpleConsumer 更灵活,PullConsumer 更推荐给流处理框架使用。(rocketmq.apache.org)
| 类型 | 特点 | 适用场景 |
|---|---|---|
| PushConsumer | SDK 封装拉取、缓存、线程池、重试和 ACK | 大多数普通业务消费 |
| SimpleConsumer | 业务主动 receive、process、ack,可控制不可见时间 | 处理时间不可控、自定义批处理、异步分发 |
| PullConsumer | 业务自己控制队列、位点、拉取 | 流处理框架或特殊消费场景 |
PushConsumer 不是 Broker 真正主动推消息,而是客户端 SDK 内部封装长轮询拉取。
consumer.registerMessageListener(new MessageListenerConcurrently() {
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, Context ctx) {
// 业务处理
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
SimpleConsumer 更像显式拉取:
receive -> process -> ack
集群消费与广播消费
RocketMQ 官方负载均衡文档区分广播消费和集群消费:广播适合网关推送、配置推送,集群消费适合微服务解耦和水平分摊流量。(rocketmq.apache.org)
| 模式 | 行为 | 场景 | 注意点 |
|---|---|---|---|
| 集群消费 | 同一个 ConsumerGroup 下多个实例共同分摊消息 | 订单处理、支付回调、视频任务、物流同步 | 常规业务默认选择 |
| 广播消费 | 同组每个实例都收到全量消息 | 配置推送、本地缓存刷新、网关通知 | 4.x 广播模式失败后不提供普通集群重试语义 |
队列级负载均衡与消息级负载均衡
4.x / 3.x 经典模型主要是队列级负载均衡:一个 MessageQueue 同一时间通常分给一个消费者。如果 Topic 只有 4 个队列,启动 20 个消费者,也只有 4 个消费者能真正消费,其余可能空闲。(rocketmq.apache.org)
TopicA: Queue0 Queue1 Queue2 Queue3
ConsumerGroupA:
Consumer1 -> Queue0
Consumer2 -> Queue1
Consumer3 -> Queue2
Consumer4 -> Queue3
Consumer5 -> 无队列可分配
5.x 的 PushConsumer 和 SimpleConsumer 默认采用消息级负载均衡。Broker 会在消费者收到消息后锁定消息,使其在消费完成或超时前对其他消费者不可见。(rocketmq.apache.org)
Rebalance 触发时机:
- Consumer 扩容;
- Consumer 缩容;
- Broker 扩缩容;
- Topic 队列变化;
- 网络抖动导致客户端上下线;
- 订阅关系变化。
为什么 Rebalance 会导致重复?
ConsumerA 拉到消息但还没提交成功
-> Rebalance 发生
-> Queue 被分给 ConsumerB
-> ConsumerB 重新拉取旧位点附近消息
-> 少量重复消费
所以负载均衡题一定要主动补一句:消费者必须幂等。
消费位点 Offset
Offset 是消费者在 MessageQueue 上的消费进度。RocketMQ 消息消费后不会立即从队列删除,而是按 ConsumerGroup 记录最新消费进度;客户端重启后可以基于服务端保存的 ConsumerOffset 恢复。(rocketmq.apache.org)
| 模式 | 位点管理 |
|---|---|
| 集群消费 | 位点通常由 Broker 按 ConsumerGroup 维护 |
| 广播消费 | 位点通常由客户端本地维护 |
| PushConsumer | SDK 封装提交 |
| SimpleConsumer | 业务 receive 后显式 ack |
消费延迟可以理解为:
Lag = MaxOffset - ConsumerOffset
Reset Offset 可以把消费进度重置到指定 offset 或时间点,但会触发历史冷数据读取,增加 Broker、磁盘和下游压力,需要谨慎授权和灰度操作。(rocketmq.apache.org)
21.5 过滤、订阅一致性与资源治理
Tag 过滤
Tag 过滤是基于字符串精确匹配,可以单 Tag、多 Tag 或 * 全匹配。(rocketmq.apache.org)
consumer.subscribe("OrderTopic", "OrderPaid || OrderRefunded");
优点:
- 简单;
- 性能好;
- 适合固定分类;
- Broker 端可以提前过滤。
限制:
- 表达能力有限;
- 不适合复杂条件;
- 需要 Producer 规范设置 Tag。
SQL92 属性过滤
SQL92 过滤基于消息属性:
Message msg = new Message("OrderTopic", body);
msg.putUserProperty("region", "CN");
msg.putUserProperty("amount", "1200");
消费者订阅:
consumer.subscribe("OrderTopic", MessageSelector.bySql("region = 'CN' and amount > 1000"));
4.x PushConsumer 文档说明 SQL92 过滤需要 Broker 端开启 enablePropertyFilter=true,默认值为 false。(rocketmq.apache.org)
订阅一致性
RocketMQ 5.x 消息过滤文档明确说明过滤表达式是订阅的一部分,同一个 ConsumerGroup 内订阅和过滤表达式需要一致。(rocketmq.apache.org)
错误示例:
ConsumerGroupA:
Consumer1 subscribe OrderTopic TagA
Consumer2 subscribe OrderTopic TagB
同一个 ConsumerGroup 内订阅不一致,可能导致部分消息被某些实例过滤掉,消费行为不可预期。
面试模板:
Tag 过滤适合稳定、简单的业务分类;SQL92 适合基于属性的复杂过滤,但要开启 Broker 配置并注意性能和订阅一致性。同一个 ConsumerGroup 内 Topic、Tag、SQL 表达式必须一致,否则会出现消息分配和过滤语义混乱。
21.6 顺序消息与乱序治理
RocketMQ 保证什么顺序
RocketMQ 顺序消息通常是局部顺序,不是全局顺序。官方顺序消息文档说明,RocketMQ 使用 MessageGroup 确定顺序,同一 MessageGroup 内 FIFO,不同 MessageGroup 不保证顺序。(rocketmq.apache.org)
OrderId = 1001:
Created -> Paid -> Shipped -> Finished
同一个订单的事件要进入同一个 MessageQueue 或同一个 MessageGroup,并被顺序消费。
顺序消息要同时满足两端
生产端:
同一业务 key
-> 固定路由到同一队列 / MessageGroup
-> 尽量单 Producer 串行发送
消费端:
同一队列 / MessageGroup
-> 同一时刻只由一个线程按顺序处理
-> 失败时暂停当前队列或当前 MessageGroup
4.x PushConsumer 中,并发消费通过 MessageListenerConcurrently,顺序消费通过 MessageListenerOrderly。并发消费可能多个线程同时消费同一个队列消息,即使发送端保证 FIFO,也不保证实际有序。(rocketmq.apache.org)
乱序来源与治理
| 乱序来源 | 治理方式 |
|---|---|
| 发送端并发发送 | 同一业务 key 串行发送,或按版本号兜底 |
| 同一业务 key 进入不同队列 | 用 ShardingKey / MessageGroup 固定路由 |
| 消费端并发处理 | 使用顺序消费,不要异步处理后提前 ACK |
| Rebalance | 业务侧使用状态机和版本号防乱序 |
| 重试消息晚于后续消息到达 | 消费端判断业务状态,不能盲目覆盖新状态 |
状态机兜底示例:
UPDATE orders
SET status = 'PAID'
WHERE order_id = ?
AND status = 'CREATED';
即使重复或乱序消息到达,也不会把 SHIPPED 回退成 PAID。
面试模板:
RocketMQ 能做的是同一业务 key 的局部顺序。生产端要把同一 key 路由到同一队列或 MessageGroup,消费端要顺序处理并在业务成功后 ACK。但分布式系统里仍可能因重试、Rebalance、异步处理带来乱序,所以核心状态类业务还要用版本号、状态机和幂等更新兜底。
21.7 可靠投递、重试、死信与幂等
对应主讲章节:第 8 章:端到端消息可靠性、重试、死信队列与消费幂等、第 4 章:Producer 发送模型、路由选择、重试机制与底层发送链路、第 5 章:Consumer 类型、长轮询、POP、ACK 与完整消费链路。
先讲投递语义
RocketMQ FAQ 明确说明消息至少投递一次,大多数情况下不会重复,但并不承诺绝对只投递一次。(rocketmq.apache.org)
所以面试里不要说“RocketMQ 保证消息不会重复消费”。更准确的说法是:
RocketMQ 提供 at-least-once 投递语义,中间件尽力保证消息不丢,但重复消费需要业务侧幂等解决。端到端 exactly-once 涉及 MQ、网络、消费者、业务数据库多个故障域,不能只靠 MQ 保证。
可靠投递链路
可靠性要按端到端链路讲:
| 位置 | 机制 | 风险 |
|---|---|---|
| Producer | 同步发送、可靠异步发送、发送重试、本地表 / Outbox | 发送结果未知、网络超时、应用崩溃 |
| Broker | CommitLog 持久化、刷盘策略、主从复制 | PageCache 未刷盘、主从未复制、磁盘故障 |
| Consumer | 业务成功后 ACK、失败重试、死信队列 | ACK 丢失、消费超时、下游异常 |
| 业务侧 | 幂等、状态机、补偿、对账 | 重复消息、乱序消息、部分成功 |
RocketMQ 最佳实践也建议,如果业务要求消息不丢,发送失败时可将消息存入 DB,再由后台线程定期重试,确保消息到达 Broker。(rocketmq.apache.org)
消费重试与死信队列
消费失败时,RocketMQ 不会立刻丢弃消息,而是按重试策略重新投递。超过最大重试次数后,消息进入死信队列 DLQ。(rocketmq.apache.org)
触发消费重试的典型情况:
- Consumer 返回失败状态;
- Listener 抛异常;
- 消费超时;
- 下游服务临时不可用;
- 业务状态未准备好。
4.x 并发消费失败:
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
设置最大重试次数:
consumer.setMaxReconsumeTimes(10);
4.x PushConsumer 文档说明:集群模式下消费重试有效,广播模式不提供普通重试;并发消费失败会进入 %RETRY%ConsumerGroupName,达到最大次数后进入 %DLQ%ConsumerGroupName。(rocketmq.apache.org)
DLQ 的处理方式:
- 先查失败原因,不要直接重放;
- 修复下游服务、数据或代码;
- 按业务 key 小批量重放;
- 重放过程继续保证幂等;
- 对无法修复的毒丸消息做人工确认和归档。
为什么会重复消费
| 来源 | 例子 |
|---|---|
| 发送重试 | Producer 超时但 Broker 实际写入成功,Producer 又重试了一次 |
| ACK 丢失 | Consumer 业务成功,但 ACK 返回失败 |
| 消费超时 | Consumer 处理太慢,Broker 认为消费失败 |
| Rebalance | 队列重新分配时,新消费者从旧位点附近重新拉取 |
| Broker 故障恢复 | 切换或恢复时可能重复投递 |
幂等设计方案
方案 1:消费记录表。
CREATE TABLE mq_consume_log (
consumer_group varchar(128) NOT NULL,
message_key varchar(256) NOT NULL,
status varchar(32) NOT NULL,
created_at timestamp NOT NULL,
PRIMARY KEY (consumer_group, message_key)
);
消费逻辑:
开始本地事务
-> 插入 consume_log,唯一键冲突说明处理过
-> 执行业务更新
-> 提交事务
ACK
方案 2:业务唯一约束。
CREATE UNIQUE INDEX uk_user_coupon
ON user_coupon(user_id, coupon_id);
重复发券时唯一键冲突,业务判断为已处理。
方案 3:状态机 CAS。
UPDATE orders
SET status = 'PAID'
WHERE order_id = ?
AND status = 'CREATED';
方案 4:Redis SETNX。
SETNX mq:consume:{group}:{messageKey} 1
EXPIRE 7 days
适合通知、日志、弱一致高吞吐场景;核心资金业务不要只依赖 Redis。
方案 5:Outbox / Inbox。
Producer:
本地事务写业务表 + outbox_message
后台任务扫描 outbox 发送 MQ
Consumer:
本地事务写业务表 + inbox_message / consume_log
ACK 时机:
正确:业务事务提交成功后 -> ACK
错误:刚收到消息 -> 先 ACK -> 再异步处理
面试模板:
RocketMQ 是 at-least-once 模型,重复消息可能来自发送重试、ACK 丢失、消费超时、Rebalance、Broker 故障恢复。核心业务用业务唯一键、消费记录表和本地事务;状态类业务用状态机 CAS;弱一致高吞吐业务可以用 Redis SETNX;Producer 端结合 Outbox,Consumer 端结合 Inbox。ACK 必须放在业务事务成功之后。
21.8 事务消息与最终一致性
对应主讲章节:第 11 章:事务消息、Half Message、事务回查与最终一致性、第 19 章:业务架构设计、技术选型与复杂场景落地。
事务消息解决什么问题
普通消息有两个典型不一致:
情况 1:
本地订单事务成功
发送 MQ 失败
下游永远不知道订单已创建
情况 2:
MQ 发送成功
本地订单事务回滚
下游消费到一条不该存在的事件
RocketMQ 事务消息解决的是“本地事务与消息发送之间的最终一致性”。
Half Message 流程
官方 4.x 事务消息文档说明事务消息分两阶段:先发送半消息,Broker 持久化后返回 ACK;生产者执行本地事务后提交 Commit 或 Rollback;如果二次确认丢失,Broker 会回查 Producer 集群中的实例确认事务状态。(rocketmq.apache.org)
1. Producer 发送 Half Message
2. Broker 持久化半消息,此时消费者不可见
3. Broker 返回 Half Message 写入成功
4. Producer 执行业务本地事务
5. 本地事务成功 -> Commit
6. 本地事务失败 -> Rollback
7. Broker 没收到二次确认 -> 事务回查
8. Producer 根据本地事务状态返回 Commit / Rollback / Unknown
事务消息不是 XA:
| 对比 | RocketMQ 事务消息 | XA / 2PC |
|---|---|---|
| 一致性 | 最终一致性 | 强一致倾向 |
| 参与者 | 本地事务 + MQ 消息 | 多资源管理器 |
| 锁持有 | 不长时间锁多个资源 | 可能长时间持有锁 |
| 适合场景 | 订单、库存、账户等业务事件 | 少数强一致事务场景 |
面试模板:
RocketMQ 事务消息通过 Half Message、本地事务、二次确认和事务回查实现最终一致性。它不是 XA,不会把多个数据库和 MQ 做成一个强一致事务,而是保证本地事务结果最终能决定消息对消费者是否可见。生产落地时还需要本地事务状态表、回查接口幂等、回查超时策略和业务补偿。
21.9 延迟消息与定时任务
对应主讲章节:第 10 章:延迟消息、定时消息与分布式任务调度。
4.x 固定延迟级别
RocketMQ 4.x 常见延迟消息是固定延迟级别,例如:
1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
发送示例:
Message msg = new Message("OrderTopic", body);
msg.setDelayTimeLevel(16); // 30m
producer.send(msg);
4.x 官方延迟消息文档说明支持 18 个固定延迟级别。(rocketmq.apache.org)
5.x 投递时间戳
RocketMQ 5.x 延迟消息通过指定投递时间戳触发,Topic 需要设置 message.type=DELAY。(rocketmq.apache.org)
deliverTime = now + 30 minutes
相比固定延迟级别,投递时间戳更适合:
- 任意时间延迟;
- 订单超时关闭;
- 视频转码结果检查;
- 任务延后重试;
- 定时通知。
面试边界
延迟消息适合“到某个时间触发一次”的业务动作,但不是完整调度系统:
| 问题 | 说明 |
|---|---|
| 精度 | 分布式系统延迟触发不是强实时闹钟 |
| 取消修改 | 已发送的延迟消息通常不适合频繁修改取消,需要业务状态兜底 |
| 幂等 | 到期消息可能重复投递,消费者仍要幂等 |
| 大规模任务 | 海量复杂调度可能需要 Scheduler + MQ 或 Redis ZSet + MQ |
面试模板:
4.x 延迟消息基于固定 DelayLevel,5.x 支持基于投递时间戳的延迟消息。它适合订单超时关闭、任务延后检查等场景,但不是强实时调度器。生产上要考虑延迟精度、取消修改、重复投递、积压和消费者幂等。
21.10 存储、持久化与写入性能
对应主讲章节:第 7 章:存储引擎:CommitLog、ConsumeQueue、IndexFile 与文件恢复、第 14 章:性能优化、流控、压测与容量规划。
三个核心文件结构
RocketMQ 使用 CommitLog 存储消息主体和元数据,ConsumeQueue 作为逻辑消费索引,IndexFile 支持通过 key 或时间区间查询;Broker 通过后台 ReputMessageService 异步构建 ConsumeQueue 和 IndexFile。(GitHub)
| 结构 | 存什么 | 作用 |
|---|---|---|
| CommitLog | 消息主体和元数据 | 所有 Topic 混合顺序追加写,是主存储 |
| ConsumeQueue | CommitLog offset、消息大小、tag hash | 按 Topic + Queue 建立消费索引 |
| IndexFile | key hash、CommitLog offset、时间差 | 支持按 key 或时间查询消息 |
写入链路:
Producer -> Broker
-> Append CommitLog
-> 返回写入结果
-> 后台构建 ConsumeQueue
-> 后台构建 IndexFile
为什么这样设计?
- 对 Producer 友好:CommitLog 顺序追加写,磁盘吞吐高;
- 对 Consumer 友好:ConsumeQueue 很小,只保存索引,顺序扫描快;
- 对查询友好:IndexFile 支持按 key 定位 CommitLog。
刷盘策略
RocketMQ 存储设计文档说明,同步刷盘要等消息真正持久化到磁盘才返回成功 ACK;异步刷盘写入 PageCache 后即可返回,后台线程再刷盘。(GitHub)
| 策略 | 行为 | 优点 | 风险 |
|---|---|---|---|
| SYNC_FLUSH | 写入磁盘后再返回成功 | 可靠性更高 | RT 高,吞吐低 |
| ASYNC_FLUSH | 写入 PageCache 后返回成功 | 延迟低,吞吐高 | 机器宕机可能丢 PageCache 中未刷盘数据 |
面试不要只说“同步刷盘一定最好”。更完整的回答是:
核心资金链路可以考虑同步刷盘、同步复制;普通业务事件常用异步刷盘换吞吐,但要接受极端宕机下的少量丢失风险,并用补偿和对账兜底。
主从复制策略
| 策略 | 行为 | 取舍 |
|---|---|---|
| 异步复制 | Master 返回成功后再异步复制给 Slave | 性能好,Master 宕机可能丢未复制消息 |
| 同步复制 | 等 Slave 确认后再返回成功 | 可靠性更强,RT 更高 |
5.x Controller 模式下,还可以通过 allAckInSyncStateSet、inSyncReplicas、minInSyncReplicas 等参数增强复制确认语义。(rocketmq.apache.org)
写入性能为什么高
RocketMQ 写入性能来自几件事:
- CommitLog 顺序追加写;
- PageCache 缓冲;
- mmap / MappedByteBuffer 减少用户态和内核态拷贝成本;
- ConsumeQueue 异步构建;
- 批量发送减少网络开销;
- 合理控制消息大小;
- 普通消息和 Trace 数据可以做 IO 隔离。
官方存储设计文档说明,RocketMQ 使用 PageCache 加速文件读写,并通过 MappedByteBuffer / mmap 将磁盘文件映射到用户态内存,提升读写效率。(GitHub)
消息什么时候删除
消息消费成功后不会立即物理删除,而是按保留时间、磁盘水位、文件滚动策略清理。消费进度由 ConsumerGroup 的 Offset 表示,物理消息仍可能在保留期内存在。(rocketmq.apache.org)
21.11 主从架构、高可用与故障切换
对应主讲章节:第 13 章:高可用:主从复制、刷盘策略、Controller 与自动故障切换、第 17 章:从 4.x 到 5.x 架构演进、第 16 章:安全、ACL、TLS、多租户隔离与跨集群灾备。
传统主从
RocketMQ 架构文档说明,Master-Slave 架构中 BrokerId 为 0 表示 Master,非 0 表示 Slave;Broker 负责消息存储、投递、查询和高可用。(rocketmq.apache.org)
BrokerName: broker-a
broker-a:0 Master
broker-a:1 Slave
Master 负责写入,Slave 复制数据。复制可以是同步复制或异步复制,刷盘可以是同步刷盘或异步刷盘。
典型组合:
| 组合 | 可靠性 | 性能 | 场景 |
|---|---|---|---|
| 异步刷盘 + 异步复制 | 中 | 高 | 普通业务消息 |
| 同步刷盘 + 同步复制 | 高 | 低 | 核心资金、强可靠链路 |
| 异步刷盘 + 同步复制 | 较高 | 中 | 想降低主宕机丢消息风险 |
高可用层次
| 层次 | 做法 |
|---|---|
| NameServer 高可用 | 多节点部署,客户端配置多个地址 |
| Broker 高可用 | 多 Master、多副本、主从复制 |
| 自动故障切换 | Controller / DLedger / 自动选主 |
| Consumer 高可用 | 多实例同组消费,失败重试和重新负载均衡 |
| 业务高可用 | 幂等、补偿、对账、降级、限流 |
Controller / DLedger 自动切换
5.x 引入 Controller 模式,可以支持自动主从切换。Controller 可以独立部署,也可以内嵌到 NameServer;如果需要容错,Controller 需要 3 个或更多副本并遵循 Raft 多数派协议。(rocketmq.apache.org)
自动切换不是“完全无损”的魔法,仍要看:
- 同步复制还是异步复制;
- SyncStateSet 中有哪些副本;
- 主节点故障时最后一批消息是否复制完成;
- 客户端路由刷新时间;
- 业务是否能接受短暂不可用或重复投递。
面试模板:
RocketMQ 高可用不是单点机制,而是 NameServer 多节点、Broker 多副本、主从复制、刷盘策略、Controller 自动选主、Consumer 多实例和业务幂等补偿共同组成。同步刷盘和同步复制可靠性更强但牺牲吞吐,Controller 能降低人工切主成本,但 RPO/RTO 仍取决于复制确认和故障检测收敛。
21.12 消息轨迹、积压治理与系统调优
对应主讲章节:第 15 章:可观测性、故障诊断、应急处理与生产 Runbook、第 6 章:Rebalance、消费位点、负载均衡与消息积压、第 14 章:性能优化、流控、压测与容量规划。
Message Trace 用来查什么
消息轨迹记录消息从发送到存储再到消费的关键事件。官方消息轨迹文档列出了 Producer、Consumer、Broker 三类轨迹属性,包括发送时间、是否发送成功、消费实例、是否消费成功、消息 Topic、存储位置、Key、Tag 等。(GitHub)
可用于定位:
- Producer 是否发送成功;
- 消息是否写入 Broker;
- 消息被哪个 Consumer 实例消费;
- 消费是否成功;
- 消息在哪个 Topic、Queue、Offset;
- 是否存在重复消费或失败重试。
4.x 默认 TraceTopic 是 RMQ_SYS_TRACE_TOPIC,Broker 侧需要 traceTopicEnable=true。(rocketmq.apache.org)
什么是消息堆积
消息堆积是生产速度长期大于消费速度,导致 MaxOffset - ConsumerOffset 持续扩大。RocketMQ 消费进度文档说明,ConsumerOffset 小于 MaxOffset 时,中间差值就是未消费消息;当消费慢于生产时,队列中会存在未消费消息。(rocketmq.apache.org)
生产速度:10 万条 / 分钟
消费速度:2 万条 / 分钟
结果:每分钟新增 8 万条积压
先定位,不要直接扩容
排查顺序:
| 层次 | 看什么 |
|---|---|
| Producer | 是否突增、是否批量发送、是否异常重试 |
| Broker | TPS、磁盘水位、PageCache、刷盘耗时、网络 |
| Queue | 队列数是否小于消费者数,是否存在热点队列 |
| Consumer | 消费 RT、线程池、失败率、重试量、GC |
| 下游 | DB、Redis、HTTP 服务是否瓶颈 |
| 业务 | 是否毒丸消息、顺序阻塞、热点 key |
短期止血
| 手段 | 适用条件 | 风险 |
|---|---|---|
| 扩容 Consumer | 队列数足够,瓶颈在消费者 CPU 或业务处理 | 队列数不足时扩容无效 |
| 提高消费线程数 | 单实例处理能力不足 | 下游可能被打爆 |
| 批量消费 / 批量落库 | 下游支持批处理 | 单批失败重试成本更高 |
| 限流 Producer | 消费端已到瓶颈 | 可能影响上游体验 |
| 降级非核心消息 | 非核心通知、日志、推荐类消息 | 需要业务接受丢弃或延后 |
| 临时跳过或隔离毒丸消息 | 单条消息反复失败阻塞队列 | 必须记录并后续补偿 |
长期治理
- 拆分 Topic,隔离不同业务 SLA;
- 增加 MessageQueue 数,避免消费者扩容上限太低;
- 优化 Consumer 业务逻辑,减少同步 RPC;
- 下游数据库加索引、批量写、连接池治理;
- 顺序消息按业务 key 拆热点;
- 重试不要当限流,失败要分类;
- 建立 Lag、消费 RT、失败率、DLQ、磁盘水位、刷盘耗时告警。
官方也提醒,不应把消费重试当成限流手段,否则会加重系统负担。(rocketmq.apache.org)
极端积压怎么处理
1. 暂停或限流 Producer
2. 保留现场指标和 Trace
3. 判断是否是毒丸消息、下游故障、热点队列、磁盘问题
4. 临时扩容消费者或增加队列
5. 核心消息优先恢复,非核心消息降级
6. 对历史积压分批回放
7. 事后复盘容量、限流、告警和降级策略
面试模板:
消息堆积不能一上来就扩容。先看生产速度、消费速度、Lag、消费失败率、重试量、队列数、消费者线程、下游 RT 和磁盘水位。短期用限流、扩容、批处理、隔离毒丸消息止血;长期要拆 Topic、增加队列、优化消费逻辑、治理下游瓶颈、完善告警和容量规划。
21.13 面试总口诀
对应复习章节:第 20 章:资深面试题库、追问链与模拟面试。
RocketMQ 解决的是异步解耦、削峰填谷、最终一致性和可靠事件驱动。
Producer 通过 NameServer 找 Broker,发送到 Topic 的 MessageQueue。
Broker 用 CommitLog 顺序写消息,用 ConsumeQueue 做消费索引,用 IndexFile 做查询索引。
ConsumerGroup 通过集群消费分摊流量,通过广播消费让每个实例都收到消息。
可靠性靠发送确认、刷盘、复制、ACK、重试、DLQ、幂等和补偿闭环。
顺序性靠同一业务 key 进入同一队列或 MessageGroup,并配合顺序消费。
RocketMQ 不保证端到端 exactly-once,业务必须幂等。
事务消息解决本地事务和消息发送的最终一致性,不是 XA。
堆积问题要从生产、Broker、队列数、消费者线程、下游瓶颈、重试和顺序阻塞全链路排查。
最后一版高频追问表:
| 追问 | 高分回答关键词 |
|---|---|
| 如何不丢消息 | 同步发送、发送重试、Outbox、刷盘、复制、ACK、补偿 |
| 如何避免重复消费 | 不能只靠 MQ,业务唯一键、消费记录表、状态机、Inbox |
| 如何保证顺序 | 局部顺序、MessageGroup / ShardingKey、顺序消费、状态机兜底 |
| 事务消息是不是强一致 | 不是 XA,是 Half Message + 本地事务 + 回查的最终一致性 |
| 积压怎么办 | 先定位瓶颈,再限流、扩容、批处理、隔离毒丸消息、长期容量治理 |
| RocketMQ vs Kafka | RocketMQ 偏业务消息能力,Kafka 偏日志流和流处理生态 |
| NameServer 是否存消息 | 不存业务消息,只做路由注册与发现 |
| Controller 是否参与读写 | 不参与普通消息读写,负责副本管理和自动选主 |