RocketMQ 的消息是推模式,还是拉模式?
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
-
概念辨析能力:面试官不仅仅是想知道 "推还是拉" 这个结论,更是想知道你是否清楚 Push 和 Pull 两种模式的本质区别、各自的优缺点,而不是只停留在表面。
-
原理理解深度:考察你是否了解 RocketMQ 的
DefaultMQPushConsumer内部其实是通过 "长轮询拉取" 实现的,能否透过 API 的 "推" 表象看到底层 "拉" 的本质。 -
架构设计思维:面试官想看你是否理解为什么 RocketMQ 选择这种 "推拉结合" 的设计,以及这种设计如何平衡实时性和服务端压力。
核心答案
RocketMQ 本质上是拉模式(Pull),但提供了 Push 模式的 API 封装。
准确地说:DefaultMQPushConsumer 给开发者 "推" 的体验,但底层依然是消费者主动向 Broker 拉取消息,采用的是 "长轮询"(Long Polling) 机制。
| 维度 | Pull 模式 | Push 模式 | RocketMQ 实际方案 |
|---|---|---|---|
| 主动方 | Consumer 主动拉取 | Broker 主动推送 | Consumer 主动拉取 |
| 实时性 | 低(依赖拉取间隔) | 高(消息立即可达) | 高(长轮询,接近实时) |
| 服务端压力 | 小 | 大(需维护推送连接) | 小 |
| 流量控制 | Consumer 自己掌控 | Broker 需要限流 | Consumer 自己掌控 |
| 代表实现 | DefaultMQPullConsumer | 典型 Push 模型 | DefaultMQPushConsumer(底层 Pull) |
深度解析
一、推模式 vs 拉模式对比
上图对比了 Push 和 Pull 两种消息投递模式的工作方式:
- Push 模式:Broker 有消息就主动推给 Consumer,实时性高,但 Broker 要管理所有 Consumer 的推送状态,压力较大;当 Consumer 消费慢时,还可能导致消息在 Consumer 端堆积甚至撑爆内存。
- Pull 模式:Consumer 主动向 Broker 拉取消息,流量由自己掌控,但如果没有消息就会频繁空轮询,浪费网络和 CPU 资源,实时性也不够。
二、RocketMQ 的长轮询机制
RocketMQ 聪明就聪明在:用 Pull 的方式,达到了接近 Push 的实时性。核心就是 "长轮询"(Long Polling)。
上图展示了 RocketMQ 长轮询的完整交互流程,整体分为以下几个步骤:
- 步骤 ①:Consumer 向 Broker 发送 Pull 请求,询问是否有新消息。
- Broker 内部判断:检查
ConsumeQueue中是否有新消息。如果有,立即返回;如果没有,不会立刻返回空响应,而是将请求挂起(Hold),等待新消息写入或超时(默认 15 秒)。 - 步骤 ②:一旦有新消息到来或超时,Broker 才返回响应给 Consumer。
- 步骤 ③:Consumer 收到响应后,立刻发起下一次 Pull 请求,形成持续循环。
关键设计点:
- 长轮询超时时间由参数
longPollingTimeout控制,默认 15 秒(Broker 端longPollingEnable=true开启) - 这个设计巧妙地解决了 Pull 模式 "空轮询浪费资源" 的问题——没有消息时,请求被挂起,不浪费带宽
- 同时也解决了 Push 模式 "Broker 压力大" 的问题——Consumer 主动拉取,Broker 无需维护推送状态
三、源码层面的验证
从 RocketMQ 源码可以清晰看到这个机制:
// DefaultMQPushConsumer 看名字是 "Push",实际内部用 Pull 实现
// 核心拉取逻辑在 PullAPIWrapper 中
// PullRequest 是拉取请求的封装
public class PullRequest {
private String consumerGroup; // 消费者组
private MessageQueue messageQueue; // 要拉取的队列
private long nextOffset; // 下一个拉取偏移量
private ProcessQueue processQueue; // 本地消息缓存队列
}
// PullMessageService 是拉取消息的核心线程
// 它从 pullRequestQueue 阻塞队列中取拉取任务,不断循环
public class PullMessageService extends ServiceThread {
@Override
public void run() {
while (!this.isStopped()) {
PullRequest pullRequest = this.pullRequestQueue.take();
// 拉取消息 — 本质是发送 Pull 请求给 Broker
this.pullMessage(pullRequest);
}
}
}
整个过程:
PullMessageService是一个后台线程,不断从pullRequestQueue中取出拉取请求- 每次拉取完成后,不管有没有消息,都会把新的
PullRequest放回队列 - 这就形成了一个 "拉取 → 处理 → 再拉取" 的循环,永不停止
四、为什么 RocketMQ 选择 Pull + 长轮询?
| 设计考量 | 纯 Push | 纯 Pull | Pull + 长轮询 |
|---|---|---|---|
| 实时性 | 极高 | 低(取决于轮询间隔) | 高(接近实时) |
| Broker 压力 | 大(维护推送连接 + 状态) | 小 | 小 |
| 流量控制 | Consumer 被动,易被压垮 | Consumer 主动,可控 | Consumer 主动,可控 |
| 空闲时开销 | 无 | 大(频繁空轮询) | 小(请求被挂起) |
| 适合大规模集群 | 差 | 好 | 好 |
总结一句话:RocketMQ 用 Pull + 长轮询的方式,把 Push 的实时性和 Pull 的可控性结合到了一起,是分布式消息队列中最成熟的设计方案。
五、常见误区
- 误区一:用了
DefaultMQPushConsumer就是 Push 模式。错!它只是对 Pull 的封装,让你用起来像 Push,底层依然是拉。 - 误区二:
DefaultMQPullConsumer已经被废弃了。在 RocketMQ 5.x 中确实不推荐直接使用 PullConsumer,推荐使用DefaultMQPushConsumer或新的SimpleConsumer。 - 误区三:长轮询会阻塞 Consumer 线程。不会。Broker 端挂起的是网络请求,Consumer 端的拉取是异步的,不会阻塞业务线程。
面试高频追问
-
追问一:长轮询的超时时间是多少?可以调整吗?
Broker 端默认 15 秒(
longPollingTimeout),可以通过 Broker 配置longPollingEnable和超时时间来调整。 -
追问二:Kafka 是推还是拉?
Kafka 也是 Pull 模式,但 Kafka 没有长轮询机制(消费者通过
poll()轮询),实时性不如 RocketMQ 的长轮询方案。 -
追问三:RocketMQ 5.x 的 SimpleConsumer 是推还是拉?
SimpleConsumer本质上也是拉模式,提供了更灵活的消息拉取和确认机制,是 RocketMQ 5.x 推荐的新消费者 API。
常见面试变体
- 变体一:
DefaultMQPushConsumer和DefaultMQPullConsumer有什么区别? - 变体二:RocketMQ 是怎么实现 "推" 的效果的?
- 变体三:RocketMQ 的长轮询机制是怎么工作的?
记忆口诀
"名推实拉,长轮询补"——名字叫 Push,实际是 Pull,靠长轮询弥补实时性。三句话记住:Consumer 主动拉、没消息先挂着、来了再返回。
总结
RocketMQ 本质是拉模式,DefaultMQPushConsumer 只是对 Pull 的封装。其核心设计是 长轮询机制:Consumer 发起 Pull 请求,Broker 没有消息时不立即返回,而是挂起请求等待新消息,兼顾了 Push 的实时性和 Pull 的流量可控性。