RocketMQ的消息是推还是拉?

消息队列(MQ)主要支持三种消费模式:推(Push)、拉(Pull)以及5.0版本新增的POP模式。本文将重点介绍Push和Pull模式。

Push模式由服务端主动向客户端推送消息,而Pull模式则需要客户端主动向服务端轮询获取数据。

两种模式各具特点:

  • Push模式优势在于实时性较好,但若客户端未做好流量控制,当服务端推送大量消息时,可能导致客户端消息积压甚至崩溃。
  • Pull模式的优点在于客户端可根据自身处理能力控制消费节奏,但频繁拉取会给服务端带来压力,并可能造成消息处理延迟。

RocketMQ同时支持两种消费模式,开发者可通过以下两个Consumer类进行选择:

java 复制代码
public class DefaultMQPullConsumer extends ClientConfig implements MQPullConsumer {
    // 源码地址:https://github.com/apache/rocketmq/blob/develop/client/src/main/java/org/apache/rocketmq/client/consumer/DefaultMQPullConsumer.java
}

public class DefaultMQPushConsumer extends ClientConfig implements MQPushConsumer {
    // 源码地址:https://github.com/apache/rocketmq/blob/develop/client/src/main/java/org/apache/rocketmq/client/consumer/DefaultMQPushConsumer.java
}

需要注意的是,DefaultMQPullConsumer已被标记为弃用(自RocketMQ 4.6.0起),建议改用DefaultLitePullConsumer。这个改进版Pull Consumer提供了Subscribe和Assign两种更简单易用的模式。

java 复制代码
/**
 * @deprecated 默认拉取消费者。该类将于2022年移除,推荐在主动拉取消息场景中使用更优的实现{@link DefaultLitePullConsumer}
 */

需要特别说明的是:RocketMQ的Push模式本质上仍基于Pull机制实现,只是通过良好的封装让开发者感知为Push模式。

以下是RocketMQ 5.1.4版本中实现长轮询的关键代码片段,主要展示PullMessageProcessor.processRequest方法的部分逻辑:

java 复制代码
if (this.brokerController.getMessageStore() instanceof DefaultMessageStore) {
    DefaultMessageStore defaultMessageStore = (DefaultMessageStore)this.brokerController.getMessageStore();
    boolean cgNeedColdDataFlowCtr = brokerController.getColdDataCgCtrService().isCgNeedColdDataFlowCtr(requestHeader.getConsumerGroup());
    if (cgNeedColdDataFlowCtr) {
        boolean isMsgLogicCold = defaultMessageStore.getCommitLog()
            .getColdDataCheckService().isMsgInColdArea(requestHeader.getConsumerGroup(),
                requestHeader.getTopic(), requestHeader.getQueueId(), requestHeader.getQueueOffset());
        if (isMsgLogicCold) {
            ConsumeType consumeType = this.brokerController.getConsumerManager().getConsumerGroupInfo(requestHeader.getConsumerGroup()).getConsumeType();
            if (consumeType == ConsumeType.CONSUME_PASSIVELY) {
                response.setCode(ResponseCode.SYSTEM_BUSY);
                response.setRemark("This consumer group is reading cold data. It has been flow control");
                return response;
            } else if (consumeType == ConsumeType.CONSUME_ACTIVELY) {
                if (brokerAllowFlowCtrSuspend) {  // second arrived, which will not be held
                    PullRequest pullRequest = new PullRequest(request, channel, 1000,
                        this.brokerController.getMessageStore().now(), requestHeader.getQueueOffset(), subscriptionData, messageFilter);
                    this.brokerController.getColdDataPullRequestHoldService().suspendColdDataReadRequest(pullRequest);
                    return null;
                }
                requestHeader.setMaxMsgNums(1);
            }
        }
    }
}

这段代码通过创建轮询任务实现长轮询机制:

java 复制代码
PullRequest pullRequest = new PullRequest(request, channel, 1000,
                    this.brokerController.getMessageStore().now(), requestHeader.getQueueOffset(), subscriptionData, messageFilter);
this.brokerController.getColdDataPullRequestHoldService().suspendColdDataReadRequest(pullRequest);

ColdDataPullRequestHoldService(继承自PullRequestHoldService)的核心运行逻辑如下:

java 复制代码
@Override
public void run() {
    log.info("{} service started", this.getServiceName());

    while (!this.isStopped()) {
        try {
            if (!this.brokerController.getMessageStoreConfig().isColdDataFlowControlEnable()) {
                this.waitForRunning(20 * 1000);
            } else {
                this.waitForRunning(5 * 1000);
            }

            long beginClockTimestamp = this.systemClock.now();
            this.checkColdDataPullRequest();
            long costTime = this.systemClock.now() - beginClockTimestamp;

            log.info("[{}] checkColdDataPullRequest-cost {} ms.", costTime > 5 * 1000 ? "NOTIFYME" : "OK", costTime);

        } catch (Throwable e) {
            log.warn(this.getServiceName() + " service has exception", e);
        }
    }

    log.info("{} service end", this.getServiceName());
}

该服务会定期(5秒或20秒)执行数据拉取检查,具体实现逻辑如下:

java 复制代码
private void checkColdDataPullRequest() {
    int succTotal = 0, errorTotal = 0;
    int queueSize = pullRequestColdHoldQueue.size();

    Iterator<PullRequest> iterator = pullRequestColdHoldQueue.iterator();
    while (iterator.hasNext()) {
        PullRequest pullRequest = iterator.next();

        if (System.currentTimeMillis() >= pullRequest.getSuspendTimestamp() + coldHoldTimeoutMillis) {
            try {
                pullRequest.getRequestCommand().addExtField(NO_SUSPEND_KEY, "1");
                this.brokerController.getPullMessageProcessor().executeRequestWhenWakeup(
                    pullRequest.getClientChannel(), pullRequest.getRequestCommand());
                succTotal++;
            } catch (Exception e) {
                log.error("PullRequestColdHoldService checkColdDataPullRequest error", e);
                errorTotal++;
            }

            iterator.remove();
        }
    }

    log.info("checkColdPullRequest-info-finish, queueSize: {} successTotal: {} errorTotal: {}",
        queueSize, succTotal, errorTotal);
}

扩展知识

用法示例

以下代码示例来自RocketMQ官方文档:https://rocketmq.apache.org/

Push模式实现

java 复制代码
public class Consumer {
    public static void main(String[] args) throws InterruptedException, MQClientException {
        // 初始化消费者并设置消费者组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name");
        
        // 配置NameServer地址
        consumer.setNamesrvAddr("localhost:9876");
        // 订阅Topic并指定消息过滤标签(*表示接收所有标签)
        consumer.subscribe("TopicTest", "*");
        // 注册消息监听器处理Broker推送的消息
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                System.out.printf("%s 接收到新消息: %s %n", Thread.currentThread().getName(), msgs);
                // 返回消息消费成功状态
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        // 启动消费者
        consumer.start();
        System.out.printf("消费者已启动.%n");
    }
}

Pull模式实现

java 复制代码
public class PullConsumerTest {
    public static void main(String[] args) throws MQClientException {
        DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("please_rename_unique_group_name_5");
        consumer.setNamesrvAddr("127.0.0.1:9876");
        consumer.start();
        
        try {
            MessageQueue mq = new MessageQueue();
            mq.setQueueId(0);
            mq.setTopic("TopicTest");
            mq.setBrokerName("jinrongtong-MacBook-Pro.local");
            
            long offset = 26;
            PullResult pullResult = consumer.pull(mq, "*", offset, 32);
            
            if (pullResult.getPullStatus().equals(PullStatus.FOUND)) {
                System.out.printf("%s%n", pullResult.getMsgFoundList());
                consumer.updateConsumeOffset(mq, pullResult.getNextBeginOffset());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        consumer.shutdown();
    }
}

LitePull模式实现

java 复制代码
public class LitePullConsumerSubscribe {
    public static volatile boolean running = true;
    
    public static void main(String[] args) throws Exception {
        DefaultLitePullConsumer litePullConsumer = new DefaultLitePullConsumer("lite_pull_consumer_test");
        litePullConsumer.subscribe("TopicTest", "*");
        litePullConsumer.setPullBatchSize(20);
        litePullConsumer.start();
        
        try {
            while (running) {
                List<MessageExt> messageExts = litePullConsumer.poll();
                System.out.printf("%s%n", messageExts);
            }
        } finally {
            litePullConsumer.shutdown();
        }
    }
}

RocketMQ 一定能实现削峰效果吗?

MQ 的三大核心优势包括:异步处理、系统解耦和削峰填谷。

在高并发场景中,系统往往会面临瞬时流量激增的问题。当请求量超过系统处理能力时,就会导致服务崩溃。通过引入 MQ 作为缓冲层,可以将突发流量暂存到消息队列中,让系统按照自身处理能力逐步消费消息。这种机制能够有效平滑流量波动,避免系统过载,实现削峰填谷的目标。

但需要注意的是,并非使用了 MQ 就一定能实现削峰效果,关键在于消息的消费模式。

特别要注意的是,在推送模式下,如果出现消费失败导致消息重投的情况,反而会加剧系统负载。因此,要实现良好的削峰效果,建议采用拉取模式消费消息

相关推荐
利刃大大1 天前
【RabbitMQ】延迟队列 && 事务 && 消息分发
分布式·消息队列·rabbitmq·队列
ghgxm5201 天前
FastApi_03_中间件 VS 依赖注入
java·中间件·fastapi
Tipriest_2 天前
ROS 2 rosbag2 播放出现 “Message queue starved. Messages will be delayed.” 的处理步骤
消息队列·ros2·缓存机制
利刃大大2 天前
【RabbitMQ】重试机制 && TTL && 死信队列
分布式·后端·消息队列·rabbitmq·队列
indexsunny2 天前
互联网大厂Java面试实战:核心技术与微服务架构解析
java·数据库·spring boot·缓存·微服务·面试·消息队列
星辰_mya2 天前
零拷贝技术之前提
rocketmq
冰冰菜的扣jio2 天前
理解RocketMQ的消息模型
java·rocketmq·java-rocketmq
河北小博博2 天前
阿里云RocketMQ和MNS(现轻量消息队列)全方位对比
阿里云·云计算·rocketmq
BHXDML2 天前
Java 常用中间件体系化解析——从单体到分布式,从“能跑”到“可控、可扩展、可演进”
java·分布式·中间件