RocketMQ顺序消费

一、什么是顺序消费

RocketMQ 支持两种类型的顺序消息消费:分区顺序消息(Partition Ordered Messages)和全局顺序消息(Global Ordered Messages)。这两种方式都能确保消息的顺序消费,但它们的工作机制有所不同。

二、分区顺序消息

在分区顺序消息中,RocketMQ 保证在同一个消息队列(Message Queue)中的消息按照发送的顺序被消费。为了实现这一点,你需要做以下几点:

  1. 消息分发:在发送消息时,你需要通过某种键(Sharding Key)将相关联的消息发送到相同的队列中。通常,这个键是业务逻辑中的一部分,比如用户ID或者订单ID。

  2. 消费控制:在消费端,你需要配置一个顺序消费的消费者(Ordered Consumer),这种消费者在处理完当前的消息之前不会接收到下一个消息。RocketMQ 会确保每个队列只被一个线程消费,从而保证顺序。

三、全局顺序消息

全局顺序消息比分区顺序消息更进一步,它不仅要求在同一个队列中的消息有序,而且要求整个主题的所有消息都按照发送的顺序被消费。这意味着所有的消息都必须进入同一个队列中,这在高并发场景下可能会成为一个瓶颈。

为了实现全局顺序消息,你同样需要配置一个顺序消费的消费者,但是你只能有一个消息队列,所以所有消息都会被发送到这个队列中,从而确保全局顺序。

实现顺序消费的步骤

  1. 配置消费者:在消费者的配置中,你需要明确指定这是一个顺序消费者。这可以通过实现MQPushConsumer接口并重写consumeMessage方法来完成,同时需要在setConsumeFromWheresubscribe方法中正确配置。

  2. 消息发送:在发送消息时,使用Sharding Key来确保相关联的消息被发送到相同的队列中(对于分区顺序消息)。

  3. 处理消息:在consumeMessage方法中,确保按顺序处理消息。RocketMQ 的设计会保证消息在一个队列中的顺序消费。

注意事项

  • 性能影响:顺序消费会降低系统的吞吐量,因为它限制了并行处理的能力。

  • 错误处理:如果在消费过程中发生错误,需要妥善处理,以避免阻塞后续消息的消费。

在实际应用中,你可能需要根据业务需求权衡是否需要顺序消费,以及选择哪种顺序消费策略。在某些情况下,分区顺序消息可能已经足够满足业务需求,而无需使用全局顺序消息。

四、实现例子

全局顺序和分区顺序,主要根据推送设置hashKey来实现的。

发送消息:

下面简单实现一个并发推送消息,来验证消息的顺序处理。

private static final String TOPIC = "TEST_TOPIC";

@Resource
private RocketMQTemplate rocketMQTemplate;

@Test
public void send(){
    VolatileExample example = new VolatileExample();

    Thread thread1 = new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            int temp = example.increment();
            Test test= new Test();
            test.setNum(temp+"");
            // 指定相同的hashKey, 确保所有消息推送到同一个队列
            // 如何消息中用关联id,例如订单id, 可以将订单id作用hashKey, 就能够实现分区顺序
            rocketMQTemplate.syncSendOrderly(TOPIC, test, "hashKey");
            System.out.println(temp);
        }
    });
    Thread thread2 = new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            int temp = example.increment();
            Test test= new Test();
            test.setNum(temp+"");
            // 指定相同的hashKey, 确保所有消息推送到同一个队列
            // 如何消息中用关联id,例如订单id, 可以将订单id作用hashKey, 就能够实现分区顺序
            rocketMQTemplate.syncSendOrderly(TOPIC, test, "hashKey");
            System.out.println(temp);
        }
    });
    Thread thread3 = new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            int temp = example.increment();
            Test test= new Test();
            test.setNum(temp+"");
            // 指定相同的hashKey, 确保所有消息推送到同一个队列
            // 如何消息中用关联id,例如订单id, 可以将订单id作用hashKey, 就能够实现分区顺序
            rocketMQTemplate.syncSendOrderly(TOPIC, test, "hashKey");
            System.out.println(temp);
        }
    });

    thread1.start();
    thread2.start();
    thread3.start();
    try {
        thread1.join();
        thread2.join();
        thread3.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println("Counter: " + example.counter);



}

static class VolatileExample {
    volatile int counter = 0;

    public synchronized int increment() {
        return counter++;
    }
}

消费消息:

消息消费默认是并发消费,并发消费无法保证顺序性,所以需要修改配置,设置顺序消费。

/**  ConsumeMode.ORDERLY  顺序消费  */
@RocketMQMessageListener(topic = "TEST_TOPIC", nameServer = "${rocketmq.name-server}", consumerGroup = "TEST_GROUP" , consumeMode = ConsumeMode.ORDERLY)
@Component
@Slf4j
public class UsersComsumerListener implements RocketMQListener<String> {


    @Override
    public void onMessage(String msg) {
        // 事件处理
        log.info("MQ消息:{}", msg);
    }

}

五、源码分析

/**
     * Same to {@link #syncSend(String, Object)} with send orderly with hashKey by specified.
     *
     * @param destination formats: topicName:tags
     * @param payload     the Object to use as payload
     * @param hashKey     use this key to select queue. for example: orderId, productId ...
     * @return {@link SendResult}
     */
    public SendResult syncSendOrderly(String destination, Object payload, String hashKey) {
        // hashKey 决定消息推送到哪个queue
        return syncSendOrderly(destination, payload, hashKey, producer.getSendMsgTimeout());
    }

// 可以根据推送源码查询下去,看到选择队列的方法

方法链路:

org.apache.rocketmq.spring.core.RocketMQTemplate#syncSendOrderly(java.lang.String, java.lang.Object, java.lang.String)

org.apache.rocketmq.spring.core.RocketMQTemplate#syncSendOrderly(java.lang.String, java.lang.Object, java.lang.String, long)

org.apache.rocketmq.spring.core.RocketMQTemplate#syncSendOrderly(java.lang.String, org.springframework.messaging.Message<?>, java.lang.String, long)

org.apache.rocketmq.client.producer.DefaultMQProducer#send(org.apache.rocketmq.common.message.Message, org.apache.rocketmq.client.producer.MessageQueueSelector, java.lang.Object, long)

org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#send(org.apache.rocketmq.common.message.Message, org.apache.rocketmq.client.producer.MessageQueueSelector, java.lang.Object, long)

org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendSelectImpl

org.apache.rocketmq.client.producer.selector.SelectMessageQueueByHash#select

找到了选择队列的方法:

@Override
    public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
        int value = arg.hashCode();
        if (value < 0) {
            // 小于0时,取绝对值,为正数
            value = Math.abs(value);
        }
        // 根据队列数,模运算找到对应的队列数(标识)
        value = value % mqs.size();
        // 根据hashCode选择队列
        return mqs.get(value);
    }

设置消费者注解

@RocketMQMessageListener 中 consumeMode 设置了顺序消息 ConsumeMode.ORDERLY

看一下 ConsumeMode.ORDERLY  是如何实现顺序消费

org.apache.rocketmq.spring.autoconfigure.ListenerContainerConfiguration#afterSingletonsInstantiated

org.apache.rocketmq.spring.autoconfigure.ListenerContainerConfiguration#registerContainer

org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer

在初始化消费者方法

org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer#initRocketMQPushConsumer

private void initRocketMQPushConsumer() throws MQClientException {
        Assert.notNull(rocketMQListener, "Property 'rocketMQListener' is required");
        Assert.notNull(consumerGroup, "Property 'consumerGroup' is required");
        Assert.notNull(nameServer, "Property 'nameServer' is required");
        Assert.notNull(topic, "Property 'topic' is required");

        RPCHook rpcHook = RocketMQUtil.getRPCHookByAkSk(applicationContext.getEnvironment(),
            this.rocketMQMessageListener.accessKey(), this.rocketMQMessageListener.secretKey());
        boolean enableMsgTrace = rocketMQMessageListener.enableMsgTrace();
        if (Objects.nonNull(rpcHook)) {
            consumer = new DefaultMQPushConsumer(consumerGroup, rpcHook, new AllocateMessageQueueAveragely(),
                enableMsgTrace, this.applicationContext.getEnvironment().
                resolveRequiredPlaceholders(this.rocketMQMessageListener.customizedTraceTopic()));
            consumer.setVipChannelEnabled(false);
            consumer.setInstanceName(RocketMQUtil.getInstanceName(rpcHook, consumerGroup));
        } else {
            log.debug("Access-key or secret-key not configure in " + this + ".");
            consumer = new DefaultMQPushConsumer(consumerGroup, enableMsgTrace,
                    this.applicationContext.getEnvironment().
                    resolveRequiredPlaceholders(this.rocketMQMessageListener.customizedTraceTopic()));
        }

        String customizedNameServer = this.applicationContext.getEnvironment().resolveRequiredPlaceholders(this.rocketMQMessageListener.nameServer());
        if (customizedNameServer != null) {
            consumer.setNamesrvAddr(customizedNameServer);
        } else {
            consumer.setNamesrvAddr(nameServer);
        }
        if (accessChannel != null) {
            consumer.setAccessChannel(accessChannel);
        }
        consumer.setConsumeThreadMax(consumeThreadMax);
        if (consumeThreadMax < consumer.getConsumeThreadMin()) {
            consumer.setConsumeThreadMin(consumeThreadMax);
        }
        consumer.setConsumeTimeout(consumeTimeout);
        consumer.setInstanceName(this.name);

        switch (messageModel) {
            case BROADCASTING:
                consumer.setMessageModel(org.apache.rocketmq.common.protocol.heartbeat.MessageModel.BROADCASTING);
                break;
            case CLUSTERING:
                consumer.setMessageModel(org.apache.rocketmq.common.protocol.heartbeat.MessageModel.CLUSTERING);
                break;
            default:
                throw new IllegalArgumentException("Property 'messageModel' was wrong.");
        }

        switch (selectorType) {
            case TAG:
                consumer.subscribe(topic, selectorExpression);
                break;
            case SQL92:
                consumer.subscribe(topic, MessageSelector.bySql(selectorExpression));
                break;
            default:
                throw new IllegalArgumentException("Property 'selectorType' was wrong.");
        }

       // 在这个地方可以看到,根据不同的消费模式设置消息监听者
        switch (consumeMode) {
            case ORDERLY:
                consumer.setMessageListener(new DefaultMessageListenerOrderly());
                break;
            case CONCURRENTLY:
                consumer.setMessageListener(new DefaultMessageListenerConcurrently());
                break;
            default:
                throw new IllegalArgumentException("Property 'consumeMode' was wrong.");
        }

        if (rocketMQListener instanceof RocketMQPushConsumerLifecycleListener) {
            ((RocketMQPushConsumerLifecycleListener) rocketMQListener).prepareStart(consumer);
        }

    }

默认顺序消息侦听器

org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer.DefaultMessageListenerOrderly

实现 MessageListenerOrderly ,顺序消息监听器默认使用手动ACK的方式

public class DefaultMessageListenerOrderly implements MessageListenerOrderly {

        @SuppressWarnings("unchecked")
        @Override
        public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
            for (MessageExt messageExt : msgs) {
                log.debug("received msg: {}", messageExt);
                try {
                    long now = System.currentTimeMillis();
                    rocketMQListener.onMessage(doConvertMessage(messageExt));
                    long costTime = System.currentTimeMillis() - now;
                    log.info("consume {} cost: {} ms", messageExt.getMsgId(), costTime);
                } catch (Exception e) {
                    log.warn("consume message failed. messageExt:{}", messageExt, e);
              // 消费失败时,默认暂时挂起1秒不进行消费
                    context.setSuspendCurrentQueueTimeMillis(suspendCurrentQueueTimeMillis);
                    return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
                }
            }

            return ConsumeOrderlyStatus.SUCCESS;
        }
    }

  • 21
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值