RocketMQ顺序消息

消息有序指的是一类消息消费时,能按照发送的顺序来消费。例如:一个订单产生了三条消息分别是订单创建、订单付款、订单完成。消费时要按照这个顺序消费才能有意义,但是同时订单之间是可以并行消费的。RocketMQ可以严格的保证消息有序。

顺序消息分为全局顺序消息与分区顺序消息,全局顺序是指某个Topic下的所有消息都要保证顺序;部分顺序消息只要保证每一组消息被顺序消费即可。

  • 全局顺序 对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。 适用场景:性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的场景
  • 分区顺序 对于指定的一个 Topic,所有消息根据 sharding key 进行区块分区。 同一个分区内的消息按照严格的 FIFO 顺序进行发布和消费。 Sharding key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Key 是完全不同的概念。 适用场景:性能要求高,以 sharding key 作为分区字段,在同一个区块中严格的按照 FIFO 原则进行消息发布和消费的场景。

RocketMQ可以严格的保证消息有序。但这个顺序,不是全局顺序,只是分区(queue)顺序。要全局顺序只能一个分区。

非顺序的原因,是因为发送消息的时候,消息发送默认是会采用轮询的方式发送到不通的queue(分区)。而消费端消费的时候,是会分配到多个queue的,多个queue是同时拉取提交消费。如果是轮询的生产者,加上同时的多线程读取,自然不能保证顺序。

如果把一个分区顺序投放在同一个分区里面,RocketMQ的确是能保证FIFO的。那么要做到顺序消息,应该怎么实现呢——把消息确保投递到同一条queue。

比如一个订单,根据同一个id,把创建-扣款(买家)-付款(给商家)的三条消息,按顺序推入一个队列,那消费的时候也是顺序执行的。

关键代码,按订单ID放入同一分区:

producer.send(msg, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                    Long id = (Long) arg;  //根据订单id选择发送queue
                    long index = id % mqs.size();
                    return mqs.get((int) index);
                }
            }, orderList.get(i).getOrderId());//订单id

 

完整的生产者

package com.xin.rocketmq.demo.testrun;

import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.MessageQueueSelector;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * Producer,发送顺序消息
 */
public class Producer {

    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");

        producer.setNamesrvAddr("192.168.10.11:9876");

        producer.start();

        String[] tags = new String[]{"TagA", "TagC", "TagD"};

        // 订单列表
        List<OrderStep> orderList = new Producer().buildOrders();

        Date date = new Date();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String dateStr = sdf.format(date);
        for (int i = 0; i < 10; i++) {
            // 加个时间前缀
            String body = dateStr + " Hello RocketMQ " + orderList.get(i);
            Message msg = new Message("TopicTest", tags[i % tags.length], "KEY" + i, body.getBytes());

            SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                    Long id = (Long) arg;  //根据订单id选择发送queue
                    long index = id % mqs.size();
                    return mqs.get((int) index);
                }
            }, orderList.get(i).getOrderId());//订单id

            System.out.println(String.format("SendResult status:%s, queueId:%d, body:%s",
                    sendResult.getSendStatus(),
                    sendResult.getMessageQueue().getQueueId(),
                    body));
        }

        producer.shutdown();
    }

    /**
     * 订单的步骤
     */
    private static class OrderStep {
        private long orderId;
        private String desc;

        public long getOrderId() {
            return orderId;
        }

        public void setOrderId(long orderId) {
            this.orderId = orderId;
        }

        public String getDesc() {
            return desc;
        }

        public void setDesc(String desc) {
            this.desc = desc;
        }

        @Override
        public String toString() {
            return "OrderStep{" +
                    "orderId=" + orderId +
                    ", desc='" + desc + '\'' +
                    '}';
        }
    }

    /**
     * 生成模拟订单数据
     */
    private List<OrderStep> buildOrders() {
        List<OrderStep> orderList = new ArrayList<OrderStep>();

        OrderStep orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc("创建");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111065L);
        orderDemo.setDesc("创建");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103117235L);
        orderDemo.setDesc("创建");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111065L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103117235L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111065L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc("推送");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103117235L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);

        orderDemo = new OrderStep();
        orderDemo.setOrderId(15103111039L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);

        return orderList;
    }
}

结果把同一个id放在一个分区了

SendResult status:SEND_OK, queueId:3, body:2020-06-07 14:28:49 Hello RocketMQ OrderStep{orderId=15103111039, desc='创建'}
SendResult status:SEND_OK, queueId:1, body:2020-06-07 14:28:49 Hello RocketMQ OrderStep{orderId=15103111065, desc='创建'}
SendResult status:SEND_OK, queueId:3, body:2020-06-07 14:28:49 Hello RocketMQ OrderStep{orderId=15103111039, desc='付款'}
SendResult status:SEND_OK, queueId:3, body:2020-06-07 14:28:49 Hello RocketMQ OrderStep{orderId=15103117235, desc='创建'}
SendResult status:SEND_OK, queueId:1, body:2020-06-07 14:28:49 Hello RocketMQ OrderStep{orderId=15103111065, desc='付款'}
SendResult status:SEND_OK, queueId:3, body:2020-06-07 14:28:49 Hello RocketMQ OrderStep{orderId=15103117235, desc='付款'}
SendResult status:SEND_OK, queueId:1, body:2020-06-07 14:28:49 Hello RocketMQ OrderStep{orderId=15103111065, desc='完成'}
SendResult status:SEND_OK, queueId:3, body:2020-06-07 14:28:49 Hello RocketMQ OrderStep{orderId=15103111039, desc='推送'}
SendResult status:SEND_OK, queueId:3, body:2020-06-07 14:28:49 Hello RocketMQ OrderStep{orderId=15103117235, desc='完成'}
SendResult status:SEND_OK, queueId:3, body:2020-06-07 14:28:49 Hello RocketMQ OrderStep{orderId=15103111039, desc='完成'}

消费者

package com.xin.rocketmq.demo.testrun;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * 顺序消息消费,带事务方式(应用可控制Offset什么时候提交)
 */
public class ConsumerInOrder {

    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3");
        consumer.setNamesrvAddr("192.168.10.11:9876");
        /**
         * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费<br>
         * 如果非第一次启动,那么按照上次消费的位置继续消费
         */
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);

        consumer.subscribe("TopicTest", "TagA || TagC || TagD");

        consumer.registerMessageListener(new MessageListenerOrderly() {

            Random random = new Random();

            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                context.setAutoCommit(true);
                for (MessageExt msg : msgs) {
                    // 可以看到每个queue有唯一的consume线程来消费, 订单对每个queue(分区)有序
                    System.out.println("consumeThread=" + Thread.currentThread().getName() + "queueId=" + msg.getQueueId() + ", content:" + new String(msg.getBody()));
                }

                try {
                    //模拟业务逻辑处理中...
                    TimeUnit.SECONDS.sleep(random.nextInt(10));
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });

        consumer.start();

        System.out.println("Consumer Started.");
    }
}

消费也是分区的顺序消费

consumeThread=ConsumeMessageThread_1queueId=1, content:2020-06-07 14:28:49 Hello RocketMQ OrderStep{orderId=15103111065, desc='创建'}
consumeThread=ConsumeMessageThread_2queueId=3, content:2020-06-07 14:28:49 Hello RocketMQ OrderStep{orderId=15103111039, desc='创建'}
consumeThread=ConsumeMessageThread_2queueId=3, content:2020-06-07 14:28:49 Hello RocketMQ OrderStep{orderId=15103111039, desc='付款'}
consumeThread=ConsumeMessageThread_1queueId=1, content:2020-06-07 14:28:49 Hello RocketMQ OrderStep{orderId=15103111065, desc='付款'}
consumeThread=ConsumeMessageThread_2queueId=3, content:2020-06-07 14:28:49 Hello RocketMQ OrderStep{orderId=15103117235, desc='创建'}
consumeThread=ConsumeMessageThread_1queueId=1, content:2020-06-07 14:28:49 Hello RocketMQ OrderStep{orderId=15103111065, desc='完成'}
consumeThread=ConsumeMessageThread_2queueId=3, content:2020-06-07 14:28:49 Hello RocketMQ OrderStep{orderId=15103117235, desc='付款'}
consumeThread=ConsumeMessageThread_2queueId=3, content:2020-06-07 14:28:49 Hello RocketMQ OrderStep{orderId=15103111039, desc='推送'}
consumeThread=ConsumeMessageThread_2queueId=3, content:2020-06-07 14:28:49 Hello RocketMQ OrderStep{orderId=15103117235, desc='完成'}
consumeThread=ConsumeMessageThread_2queueId=3, content:2020-06-07 14:28:49 Hello RocketMQ OrderStep{orderId=15103111039, desc='完成'}

 消费者将锁定每个消息队列,以确保他们被逐个消费,虽然这将会导致性能下降,但是当你关心消息顺序的时候会很有用。我们不建议抛出异常,你可以返回 ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT 作为替代。

posted @ 2020-06-07 14:34  昕友软件开发  阅读(1712)  评论(0编辑  收藏  举报
欢迎访问我的开源项目:xyIM企业即时通讯