在分布式消息中间件领域,RocketMQ 以其高性能、低延迟和高可靠性著称。作为开发者,我们往往只需要几行代码就能完成消息的生产和消费,但在简洁的 API 背后,客户端(Client)内部运行着一套精密复杂的机制。
本文将深入剖析 RocketMQ 客户端的两大核心领域:消息发送的高可用策略与消息消费的推拉机制及重平衡(Rebalance),带你领略其源码级的设计哲学。
全文思维导图
第一部分:消息发送(Producer)—— 寻找最佳路径
生产者发送消息的核心诉求是:快且不丢。为了实现这一目标,RocketMQ 设计了一套完善的路由选择和故障规避机制。
1. 路由选择策略
当生产者发送消息时,Topic 通常分布在多个 Broker 的多个 MessageQueue 上。Producer 需要从 TopicPublishInfo 中选择一个队列进行发送。
A. 默认策略:轮询(Round Robin)
在无故障情况下,RocketMQ 默认采用轮询机制。Producer 内部维护一个线程安全的 index,每次发送时递增,对队列总数取模。这保证了消息能够均匀分布在所有队列上,从而实现负载均衡。
// 源码逻辑简述 (TopicPublishInfo.java)
public MessageQueue selectOneMessageQueue(final String lastBrokerName) {
// 简单的轮询算法
int index = this.sendWhichQueue.incrementAndGet();
int pos = Math.abs(index) % this.messageQueueList.size();
return this.messageQueueList.get(pos);
}
B. 故障延迟机制(Fault Latency Mechanism)
这是生产环境高可用的关键。 如果开启了 sendLatencyFaultEnable = true(默认关闭,建议开启),客户端会启用故障规避。
当向某个 Broker 发送消息失败或耗时过长时,Producer 会将该 Broker 标记为“不可用”一段时间。
- 原理:根据本次请求的耗时(currentLatency),计算该 Broker 的隔离时长(notAvailableDuration)。
| 本次发送耗时 | Broker 隔离时长 |
|---|---|
| >= 550ms | 30000ms (30s) |
| >= 1000ms | 60000ms (60s) |
| … | … |
发送流程图:
2. 如何处理发送失败?
RocketMQ 提供了自动重试机制来保证消息投递的可靠性:
- 重试次数:默认同步发送重试 2 次(共 3 次),异步发送重试 2 次。
- 规避策略:
- 在重试时,如果上一次发送失败是指向
Broker-A,那么在选择下一个队列时,Producer 会尽量避开Broker-A,选择Broker-B或其他节点的队列。 - 这一机制大大提升了在 Broker 宕机或网络抖动时的发送成功率。
- 在重试时,如果上一次发送失败是指向
第二部分:消息消费(Consumer)—— 复杂的推拉博弈
消费端是 RocketMQ 机制最复杂、也是面试中最常问的部分。
1. Push 模式的本质:披着 Push 外衣的 Pull
在代码中我们使用 DefaultMQPushConsumer,感觉像是 Broker 主动把消息推给客户端。但实际上,RocketMQ 的 Push 模式是基于 Long Polling(长轮询)的 Pull 模式实现的。
为什么不用纯 Push?
纯 Push 模式下,Broker 难以掌控消费者的处理能力。如果 Broker 推送速度远超消费者处理速度,会造成消费者内存溢出(OOM)。
为什么不用纯 Pull?
纯 Pull 需要消费者不断轮询,间隔时间难定。间隔短了浪费带宽和 CPU,间隔长了消息延迟大。
RocketMQ 的长轮询(Long Polling)
- 发起请求:Consumer 发送拉取请求(PullRequest)给 Broker。
- Broker 挂起:Broker 收到请求,检查是否有新消息。
- 有消息:立即返回。
- 无消息:不立即返回空,而是将请求挂起(Suspend)。
- 等待与唤醒:
- Broker 会持有该请求最多 15秒(默认)。
- 在此期间,如果 CommitLog 有新消息写入,ReputMessageService 会通知唤醒请求,立即返回消息。
- 如果超时(15s)仍无消息,返回空,Consumer 收到后立马再次发起请求。
Push 模式时序图:
2. Flow Control(流控):保护消费者的最后一道防线
既然本质是 Pull,RocketMQ 就可以在客户端做流控。在 PullMessageService 准备去 Broker 拉取消息之前,会先检查 ProcessQueue(本地处理队列)的状态。
如果出现以下情况,消费者会暂停拉取(延迟 50ms 后再试):
- 数量堆积:本地缓存的消息数 > 1000 条(默认)。
- 内存占用:本地缓存的消息总大小 > 100MB(默认)。
- 跨度过大:本地缓存的消息最小 Offset 和最大 Offset 差值 > 2000(防止大量未被消费的消息堆积,通常发生在顺序消费场景)。
// 源码逻辑简述 (DefaultMQPushConsumerImpl.java)
if (cachedMessageCount > this.defaultMQPushConsumer.getPullThresholdForQueue()) {
// 触发流控,延迟 50ms 再放入拉取任务
this.executePullRequestLater(pullRequest, 50);
return;
}
3. Rebalance(重平衡)机制
Rebalance 是 RocketMQ 协调消费者扩缩容的核心机制。它的目标是将 Topic 下的所有 MessageQueue 尽可能均匀地分配给 Consumer Group 下的所有 Consumer。
A. 什么时候触发?
- 消费者变动:新的 Consumer 上线或旧的 Consumer 下线(宕机)。
- 订阅关系变动:订阅的 Topic 发生变化。
- 队列扩容:Topic 的 MessageQueue 数量发生变化。
B. 分配策略
RocketMQ 提供了多种策略,最常用的是:
- 平均分配(AllocateMessageQueueAveragely):
- 类似于分页算法。
- 例如:8个队列,3个消费者。C1 分 3 个,C2 分 3 个,C3 分 2 个。
- 环形分配(AllocateMessageQueueAveragelyByCircle):
- 轮流分发。
- 例如:8个队列,3个消费者。
- Q0 -> C1, Q1 -> C2, Q2 -> C3
- Q3 -> C1, Q4 -> C2, Q5 -> C3 …
Rebalance 视觉演示:
C. Rebalance 的局限性
在 Rebalance 过程中(通常约 20s),可能会导致短暂的消息重复消费。因为在旧的消费者停止拉取、新的消费者开始拉取之间,Offset 的提交可能存在时差。因此,消费业务逻辑必须保证幂等性。
4. Offset 管理:从哪里跌倒,从哪里爬起
消费者消费完消息后,需要记录进度(Offset),以便宕机重启后继续消费。
- 广播模式(Broadcasting):
- 存储位置:消费者本地文件。
- 原因:广播模式下,每条消息都要被每个消费者消费,各个消费者进度互不影响,没必要上传 Broker。
- 集群模式(Clustering):
- 存储位置:Broker 端。
- 原因:同一个 Group 下的消费者共同消费一个 Topic,某个消费者宕机,其他消费者接手队列时,需要知道从哪里开始继续消费。Offset 存储在 Broker 的
config/delayOffset.json和内存中。
总结
RocketMQ 客户端的设计极其精妙:
- 发送端通过故障延迟机制实现了高可用,聪明地避开不稳定的 Broker。
- 消费端利用长轮询机制,兼顾了 Push 的实时性和 Pull 的可控性。
- 流控机制在客户端筑起了一道防洪堤,防止下游服务被压垮。
- Rebalance 动态调整队列映射,实现了分布式系统的弹性伸缩。
理解这些底层机制,不仅能帮助我们写出更健壮的代码,也能在遇到消息堆积、重复消费等生产问题时,快速定位并解决问题。
2131



被折叠的 条评论
为什么被折叠?



