RocketMQ 的推模式、拉模式,到底有什么区别?
RocketMQ 客户端提供了DefaultMQPushConsumer和DefaultLitePullConsumer两种 API,但底层都是拉取模型。所谓 Push,本质上是“封装好的、带流控的长轮询拉取”。
一、经典推拉模型
这里讨论的是 Consumer 与 Broker 之间的交互,不是 Producer 与 Broker。
经典推模式
Broker 收到消息后主动推送给 Consumer。Consumer 被动接收。
缺点很明显:当 Broker 推送速度超过消费能力时,Consumer 会内存积压,甚至 OOM。因此经典推模式只适合消息量不大、消费能力强的场景。
经典拉模式
Consumer 主动向 Broker 拉取消息,消费多少拉多少。消费不过来时可以暂停或降低频率。
代价是可能引入消息延迟和 Broker 端积压。RocketMQ 和 Kafka 都选择拉模型,所以必须做好积压监控。
二、RocketMQ 的 PushConsumer 本质还是拉
客户端实例会启动多个异步服务:
RebalanceService:负责队列再平衡,生成PullRequest;PullMessageService:从pullRequestQueue取请求并拉取消息;ConsumeMessageOrderlyService或ConsumeMessageConcurrentlyService:消费消息。
拉取消息前会做流控。如果缓存消息超过 1000 条或 100MB,就延迟 50 毫秒再拉:
if(cachedMessageCount>this.defaultMQPushConsumer.getPullThresholdForQueue()){this.executePullRequestLater(pullRequest,PULL_TIME_DELAY_MILLS_WHEN_FLOW_CONTROL);return;}这就是 RocketMQ Push 和经典 Push 的关键区别:不是 Broker 无脑推送,而是客户端根据自身消费能力主动控制拉取节奏。
三、Broker 没有消息时怎么办
如果 Broker 暂时没有消息,客户端不会高频空转,而是进入长轮询等待。
客户端收到NO_NEW_MSG后,会把请求重新放回队列:
caseNO_NEW_MSG:pullRequest.setNextOffset(pullResult.getNextBeginOffset());DefaultMQPushConsumerImpl.this.correctTagsOffset(pullRequest);DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest);break;Broker 端的PullRequestHoldService会定期检查是否有新消息。默认每 5 秒检查一次,长轮询整体等待时间约 30 秒。如果 CommitLog 有新数据,还会主动唤醒请求,做到准实时。
四、PUSH 模式的应用开发
官方示例大致如下:
DefaultMQPushConsumerconsumer=newDefaultMQPushConsumer("CID_JODIE_1");consumer.subscribe("TopicTest","*");consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);consumer.registerMessageListener(newMessageListenerConcurrently(){@OverridepublicConsumeConcurrentlyStatusconsumeMessage(List<MessageExt>msgs,ConsumeConcurrentlyContextcontext){System.out.printf("%s Receive New Messages: %s %n",Thread.currentThread().getName(),msgs);returnConsumeConcurrentlyStatus.CONSUME_SUCCESS;}});consumer.start();用户只需要注册监听器,拉取过程由客户端内部的线程完成。
五、PULL 模式的应用开发
DefaultLitePullConsumer的用法更接近业务自己控制拉取:
DefaultLitePullConsumerlitePullConsumer=newDefaultLitePullConsumer("lite_pull_consumer_test");litePullConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);litePullConsumer.subscribe("TopicTest","*");litePullConsumer.start();while(running){List<MessageExt>messageExts=litePullConsumer.poll();System.out.printf("%s%n",messageExts);}更完整的同步拉取示例:
DefaultMQPullConsumerconsumer=newDefaultMQPullConsumer("group1_pull");consumer.setNamesrvAddr("localhost:9876");consumer.start();Set<MessageQueue>messageQueues=consumer.fetchSubscribeMessageQueues("topic1");intmaxNums=10;while(true){booleanfound=false;for(MessageQueuemessageQueue:messageQueues){longoffset=consumer.fetchConsumeOffset(messageQueue,false);PullResultpullResult=consumer.pull(messageQueue,"tag8",offset,maxNums);switch(pullResult.getPullStatus()){caseFOUND:found=true;List<MessageExt>msgs=pullResult.getMsgFoundList();for(MessageExtmsg:msgs){// 处理消息}consumer.updateConsumeOffset(messageQueue,pullResult.getNextBeginOffset());break;caseNO_NEW_MSG:break;caseNO_MATCHED_MSG:break;caseOFFSET_ILLEGAL:break;}}if(!found){TimeUnit.MILLISECONDS.sleep(5000);}}PULL 模式中,拉取时机、拉取位置、消费进度都由业务控制。
六、Push 与 Pull 怎么选
| 维度 | PushConsumer | PullConsumer |
|---|---|---|
| 开发成本 | 低,只需注册监听器 | 高,需要自己管理队列和 offset |
| 可控性 | 低,拉取由客户端内部线程负责 | 高,可自主决定拉取时机和位置 |
| 流控 | 客户端内部自动流控 | 业务自己控制 |
| 适用场景 | 大多数常规消费场景 | 需要精确控制消费位点和批量的场景 |
如果只是常规消费,优先用 PushConsumer。如果要做复杂批处理、按指定 offset 消费、精确控制吞吐,可以选择 PullConsumer。
七、总结
RocketMQ 的 Push 不是 Broker 主动推送,而是客户端通过 RebalanceService、PullMessageService 和消费线程共同完成的自动拉取。它用流控避免消息在消费端堆积。
Pull 模式把更多控制权交给业务,代价是要自己处理队列、offset 和重试。理解这两种模式的关键,是看清“拉取请求从哪来、消费线程怎么触发、流控发生在哪里”。