6.顺序消息的发送与消费

254 阅读9分钟

4.2 顺序消息

消息有序指的是可以按照消息的发送顺序来消费(FIFO)。

RocketMQ可以严格的保证消息有序,可以分为分区有序或者全局有序。

顺序消费的原理解析,在默认的情况下消息发送会采取Round Robin轮询方式把消息发送到不同的queue(分区队列);而消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。

但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。

当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的。

下面用订单进行分区有序的示例。

一个订单的顺序流程是:创建、付款、推送、完成。

订单号相同的消息会被先后发送到同一个队列中,消费时,同一个OrderId获取到的肯定是同一个队列。

4.2.1 顺序消息生产

package com.itheima.mq.rocketmq.order;
​
import java.util.ArrayList;
import java.util.List;
​
/**
 * 订单构建者
 */
public class OrderStep {
    private long orderId;
    private String desc;
    private Long date = System.currentTimeMillis();
​
    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 + ''' +
                ", date=" + date +
                '}';
    }
​
    public static List<OrderStep> buildOrders() {
        //  1039L   : 创建    付款 推送 完成
        //  1065L   : 创建    付款
        //  7235L   : 创建    付款
        List<OrderStep> orderList = new ArrayList<OrderStep>();
​
        OrderStep orderDemo = new OrderStep();
        orderDemo.setOrderId(1039L);
        orderDemo.setDesc("创建");
        orderList.add(orderDemo);
​
        orderDemo = new OrderStep();
        orderDemo.setOrderId(1065L);
        orderDemo.setDesc("创建");
        orderList.add(orderDemo);
​
        orderDemo = new OrderStep();
        orderDemo.setOrderId(1039L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);
​
        orderDemo = new OrderStep();
        orderDemo.setOrderId(7235L);
        orderDemo.setDesc("创建");
        orderList.add(orderDemo);
​
        orderDemo = new OrderStep();
        orderDemo.setOrderId(1065L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);
​
        orderDemo = new OrderStep();
        orderDemo.setOrderId(7235L);
        orderDemo.setDesc("付款");
        orderList.add(orderDemo);
​
        orderDemo = new OrderStep();
        orderDemo.setOrderId(1065L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);
​
        orderDemo = new OrderStep();
        orderDemo.setOrderId(1039L);
        orderDemo.setDesc("推送");
        orderList.add(orderDemo);
​
        orderDemo = new OrderStep();
        orderDemo.setOrderId(7235L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);
​
        orderDemo = new OrderStep();
        orderDemo.setOrderId(1039L);
        orderDemo.setDesc("完成");
        orderList.add(orderDemo);
​
        return orderList;
    }
​
​
}
​
RocketMq提供了3种不同的选择队列方式:
SelectMessageQueueByHash implements MessageQueueSelector 
SelectMessageQueueByMachineRoom implements MessageQueueSelector 
SelectMessageQueueByRandom implements MessageQueueSelector 
package com.itheima.mq.rocketmq.order;
​
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.util.List;
​
public class Producer {
​
    public static void main(String[] args) throws Exception {
        //1.创建消息生产者producer,并制定生产者组名
        DefaultMQProducer producer = new DefaultMQProducer("group1");
        //2.指定Nameserver地址
        producer.setNamesrvAddr("127.0.0.1:9876");
        //3.启动producer
        producer.start();
        //构建消息集合
        List<OrderStep> orderSteps = OrderStep.buildOrders();
        //发送消息
        for (int i = 0; i < orderSteps.size(); i++) {
            String body = orderSteps.get(i).toString() + "---" + i  ;
            Message message = new Message("test", "order", "i" + i, body.getBytes());
            /**
             * 参数一:消息对象
             * 参数二:消息队列的选择器
             * 参数三:选择队列的业务标识(订单ID)
             */
            SendResult sendResult = producer.send(message, new MessageQueueSelector() {
            /**
             @param mqs:当前topic对应的队列集合
             @param msg:消息对象
             @param arg:业务标识的参数即orderId
             @return
            **/
        @Override
       public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                    byte[] byteArray = (byte[]) msg.getBody();
                    System.out.println(new String(byteArray));
                    long orderId = (long) arg;
                    //使用orderId对当前topic对应的队列集合个数取模 
                    //保证同一个orderId发送到同一个queue
                    long index = orderId % mqs.size();
                    return mqs.get((int) index);
                }
            }, orderSteps.get(i).getOrderId());
​
            System.out.println("发送结果:" + sendResult);
        }
        producer.shutdown();
    }
​
}
​
OrderStep{orderId=1039, desc='创建', date=1617153133257}---0
发送结果:SendResult [sendStatus=SLAVE_NOT_AVAILABLE, msgId=AC1278E72E2014DAD5DC9C785DC20000, offsetMsgId=AC1278E700002A9F0000000000005C89, messageQueue=MessageQueue [topic=test, brokerName=broker-a, queueId=7], queueOffset=38]
OrderStep{orderId=1065, desc='创建', date=1617153133257}---1
发送结果:SendResult [sendStatus=SLAVE_NOT_AVAILABLE, msgId=AC1278E72E2014DAD5DC9C785E390001, offsetMsgId=AC1278E700002A9F0000000000005D6D, messageQueue=MessageQueue [topic=test, brokerName=broker-a, queueId=1], queueOffset=32]
OrderStep{orderId=1039, desc='付款', date=1617153133257}---2
发送结果:SendResult [sendStatus=SLAVE_NOT_AVAILABLE, msgId=AC1278E72E2014DAD5DC9C785E950002, offsetMsgId=AC1278E700002A9F0000000000005E51, messageQueue=MessageQueue [topic=test, brokerName=broker-a, queueId=7], queueOffset=39]
OrderStep{orderId=7235, desc='创建', date=1617153133257}---3
发送结果:SendResult [sendStatus=SLAVE_NOT_AVAILABLE, msgId=AC1278E72E2014DAD5DC9C785EE40003, offsetMsgId=AC1278E700002A9F0000000000005F35, messageQueue=MessageQueue [topic=test, brokerName=broker-a, queueId=3], queueOffset=29]
OrderStep{orderId=1065, desc='付款', date=1617153133257}---4
发送结果:SendResult [sendStatus=SLAVE_NOT_AVAILABLE, msgId=AC1278E72E2014DAD5DC9C785F610004, offsetMsgId=AC1278E700002A9F0000000000006019, messageQueue=MessageQueue [topic=test, brokerName=broker-a, queueId=1], queueOffset=33]
OrderStep{orderId=7235, desc='付款', date=1617153133257}---5
发送结果:SendResult [sendStatus=SLAVE_NOT_AVAILABLE, msgId=AC1278E72E2014DAD5DC9C785FBF0005, offsetMsgId=AC1278E700002A9F00000000000060FD, messageQueue=MessageQueue [topic=test, brokerName=broker-a, queueId=3], queueOffset=30]
OrderStep{orderId=1065, desc='完成', date=1617153133257}---6
发送结果:SendResult [sendStatus=SLAVE_NOT_AVAILABLE, msgId=AC1278E72E2014DAD5DC9C78604B0006, offsetMsgId=AC1278E700002A9F00000000000061E1, messageQueue=MessageQueue [topic=test, brokerName=broker-a, queueId=1], queueOffset=34]
OrderStep{orderId=1039, desc='推送', date=1617153133257}---7
发送结果:SendResult [sendStatus=SLAVE_NOT_AVAILABLE, msgId=AC1278E72E2014DAD5DC9C7860A80007, offsetMsgId=AC1278E700002A9F00000000000062C5, messageQueue=MessageQueue [topic=test, brokerName=broker-a, queueId=7], queueOffset=40]
OrderStep{orderId=7235, desc='完成', date=1617153133257}---8
发送结果:SendResult [sendStatus=SLAVE_NOT_AVAILABLE, msgId=AC1278E72E2014DAD5DC9C7861060008, offsetMsgId=AC1278E700002A9F00000000000063A9, messageQueue=MessageQueue [topic=test, brokerName=broker-a, queueId=3], queueOffset=31]
OrderStep{orderId=1039, desc='完成', date=1617153133257}---9
发送结果:SendResult [sendStatus=SLAVE_NOT_AVAILABLE, msgId=AC1278E72E2014DAD5DC9C7861630009, offsetMsgId=AC1278E700002A9F000000000000648D, messageQueue=MessageQueue [topic=test, brokerName=broker-a, queueId=7], queueOffset=41]

可以看出来我们发送的消息是编号是0 - 9,对应的queueId是1,3,7。

4.2.2 顺序消费消息

如何保证顺序发送的消息被顺序消费的呢?

那就要知道每个消费者负载均衡的过程。

我们现在启动了3个消费者,每个消费者监听1个队列。

每个消费者依次从自己监听的队列上面拉取消息。

拉取的消息的顺序和发送消息的顺序是一致的。

因此就保证了顺序消费。

package com.itheima.mq.rocketmq.order;
​
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.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;
​
import java.util.List;
​
public class Consumer {
    public static void main(String[] args) throws MQClientException {
        //1.创建消费者Consumer,制定消费者组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
        //2.指定Nameserver地址
        consumer.setNamesrvAddr("127.0.0.1:9876");
        //3.订阅主题Topic和Tag
        consumer.subscribe("test", "order");
​
        //4.注册消息监听器
        consumer.registerMessageListener(new MessageListenerOrderly() {
​
            @Override
            public ConsumeOrderlyStatus consumeMessage
                (List<MessageExt> msgs, ConsumeOrderlyContext context) {
                for (MessageExt msg : msgs) {
                    System.out.println("线程名称:【" + Thread.currentThread().getName() + "】:" + new String(msg.getBody()));
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });
​
        //5.启动消费者
        consumer.start();
​
        System.out.println("消费者启动");
​
    }
}
​
启动3个消费者,可以发现消息分别被3个消费者消费:可以看到每个orderId对应的操作都被同一个消费者消费。并且是严格按照创建 付款 推送 完成的顺序推送。
​
消费者1:
线程名称:【ConsumeMessageThread_2】:OrderStep{orderId=1039, desc='创建', date=1617153133257}---0
线程名称:【ConsumeMessageThread_3】:OrderStep{orderId=1039, desc='付款', date=1617153133257}---2
线程名称:【ConsumeMessageThread_4】:OrderStep{orderId=1039, desc='推送', date=1617153133257}---7
线程名称:【ConsumeMessageThread_5】:OrderStep{orderId=1039, desc='完成', date=1617153133257}---9
​
消费者2:
线程名称:【ConsumeMessageThread_1】:OrderStep{orderId=7235, desc='创建', date=1617153133257}---3
线程名称:【ConsumeMessageThread_2】:OrderStep{orderId=7235, desc='付款', date=1617153133257}---5
线程名称:【ConsumeMessageThread_3】:OrderStep{orderId=7235, desc='完成', date=1617153133257}---8
​
消费者3:
线程名称:【ConsumeMessageThread_1】:OrderStep{orderId=1065, desc='创建', date=1617153133257}---1
线程名称:【ConsumeMessageThread_2】:OrderStep{orderId=1065, desc='付款', date=1617153133257}---4
线程名称:【ConsumeMessageThread_3】:OrderStep{orderId=1065, desc='完成', date=1617153133257}---6

4.2.3提高顺序消费并发:增加queue

队列在 RocketMQ 中是一个非常重要的概念,那队列在 RocketMQ 中的作用是什么呢?

这就要从消息队列的消费机制说起。

几乎所有的消息队列产品都使用一种非常朴素的“请求 - 确认”机制,确保消息不会在传递过程中由于网络或服务器故障丢失。

在生产端,生产者先将消息发送给服务端,也就是 Broker,服务端在收到消息并将消息写入主题或者队列中后,会给生产者发送确认的响应。

如果生产者没有收到服务端的确认或者收到失败的响应,则会【重新发送消息】。

在消费端,消费者在收到消息并完成自己的消费业务逻辑(比如,将数据保存到数据库中)后,也会给服务端发送消费成功的确认,服务端只有收到消费确认后,才认为一条消息被成功消费,否则它会给消费者重新发送这条消息,直到收到对应的消费成功确认。

这个确认机制很好地保证了消息传递过程中的可靠性,但是,引入这个机制在消费端带来了一个消息的有序性的问题?

在某一条消息被成功消费之前,下一条消息是不能被消费的,否则就会出现消息空洞,违背了有序性这个原则。 也就是说,每个主题在任意时刻,至多只能有一个消费者实例在进行消费,那就没法通过水平扩展消费者的数量来提升消费端总体的消费性能。

为了解决这个问题,RocketMQ 在主题下面增加了队列的概念。每个主题包含多个队列,通过多个队列来实现多实例并行生产和消费。

需要注意的是,RocketMQ 只在队列上保证消息的有序性,主题层面是无法保证消息的严格顺序的。如果需要主题层面和全局有序需要全局1个队列!