抛出问题

顺序消息?不是消息队列么,队列不是有序的么,不是先进先出么,对,队列是的,但是RocketMQ是对队列的升级,我们创建一个topic其实就是相当于创建了一个队列,但是这个队列是聚合了多个队列的,默认一个topic创建4个队列,如下图!

RocketMQ顺序消息_i++
那么抛开其他问题导致消息无序,如消息在网络中传输的延迟不同,那么这种架构设计的时候就是存在消息无序的,原因很简单,我们简化上面这张图!
RocketMQ顺序消息_ide_02
我们有一个生产者,可以生产Topic为test1、test2的消息,当生产者需要发送消息的时候,不考虑顺序消费的场景下,会在当前Topic下轮询给queue发消息,假设我们生产者给Topic为test1的发送5条消息,那么轮询消息就会给queue0发一条msg1,再给queue1发一条msg2,再给queue2发一条msg3,再给queue3发一条msg4,再给queue0发一条msg5,就是这样轮询的,如上图,我们还有消费端,这里消费端有三个消费者,监听关系如上如所示,这时消息已经到达了Broker中,那么这三个消费者都会收到监听事件,那么这时各自去各自订阅上的queue中读取数据,那么这时其实就会有问题,消费者监听消费的时候类似于并行的那么顺序也就会乱,有可能消费者B机器性能牛逼,网络状况好,那么先消费掉监听queue1中的数据,也就是先得到msg2,那么这时我们消费者先消费的第一条消息并不是我们生产者发出的第一条消息msg1,那么这就是无序消息,其实很多场景并不考虑消息是否有序,但是特殊场景除外!

介绍

消息有序指的是可以按照消息的发送顺序来消费(FIFO), RocketMQ可以严格的保证消息有序,可以分为分区有序或者全局有序。顺序消费的原理解析,在默认的情况下消息发送会采取Round Robin轮询方式把消息发送到不同的queue(分区队列);而消费消息的时,候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。当发送和消费参与的queue只有一个,则是全局有序;如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的.下面用订单进行分区有序的示例。一个订单的顺序流程是:创建、付款、推送、完成。订单号相同的消息会被先后发送到同一个队列中,消费时,同一个Orderld获取到的肯定是同一个队列。

我们使用的RocketMQ底层确实是使用的是队列的形式,那么我们知道队列是先进先出的结构,如果RocketMQ内部只有一个队列那么是可以保证全局顺序的,但是实际上RocketMQ内部是有多个Broker的,那么就不能保证全局顺序的!
RocketMQ顺序消息_java_03
场景再现!
RocketMQ顺序消息_其他_04
在生产端我们能保证消息投递的顺序,但是由于消费端是多线程的消费模式,难以保证在消费是也是按顺序消费的,全局的顺序消息时没必要保证的,但是我们可以针对每个订单的顺序进行控制,这个就是局部顺序!

顺序消息设计思路

我们想办法将一个订单需要发送的消息存放到一个队列中,那么这样消费者在消费的时候,那么消费者消费队列的时候采用单线程的消费,也就是消费者方一个线程对应一个队列!那么这样的话就能保证局部消息的顺序了!具体实现我们可以通过业务标识选择某个具体的队列,将同一个业务标识的消息都投放到同一个队列中即可(业务标识如ID,在订单中可以为订单ID)

代码

采用rocket-client编写如下


/**
* @description: 订单构建者
* @author TAO
* @date 2021/1/14 22:50
*/
public 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 + '\'' +
                '}';
    }

    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;
    }
}


/**
* @description: 顺序消息
* @author TAO
* @date 2021/8/30 23:21
*/
public class Producer {

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

        producer.setNamesrvAddr("192.168.1.12:9876");

        //3.启动producer
        producer.start();
        //构建消息集合
        List<OrderStep> orderSteps = OrderStep.buildOrders();
        //发送消息
        for (int i = 0; i < orderSteps.size(); i++) {
            String body = orderSteps.get(i) + "";
            Message message = new Message("OrderTopic", "Order", "i" + i, body.getBytes());
            /**
             * 参数一:消息对象
             * 参数二:消息队列的选择器
             * 参数三:选择队列的业务标识(订单ID)
             */
            SendResult sendResult = producer.send(message, new MessageQueueSelector() {
                /**
                 *
                 * @param mqs:队列集合
                 * @param msg:消息对象
                 * @param arg:业务标识的参数
                 * @return
                 */
                @Override
                public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                    Long orderId = (Long) arg;
                    long index = orderId % mqs.size();
                    return mqs.get((int) index);
                }
            }, orderSteps.get(i).getOrderId());

            System.out.println("发送结果:" + sendResult);
        }
        producer.shutdown();
    }

}


/**
* @description: 顺序消费者
* @author TAO
* @date 2021/8/30 23:44
*/
public class Consumer {
    public static void main(String[] args) throws Exception {
        //1.创建消费者Consumer,制定消费者组名
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
        //2.指定Nameserver地址
        consumer.setNamesrvAddr("192.168.1.12:9876");
        //3.订阅主题Topic和Tag
        consumer.subscribe("OrderTopic", "*");

        //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("消费者启动");
    }
}

实现效果

RocketMQ顺序消息_数据_05

问题

上面是使用使用rocketmq-client整合RocketMQ当时写的时候其实并没有发现这个问题,在写rocketmq-spring-boot-starter整合RocketMQ这篇文章的时候,对API的时候,如下RocketMQ顺序消息_ide_06
这里有同步、异步、单向的顺序消息,于是我就挨个试了一下,结果试出问题了,同步、单向消息确实是可以,但是到了异步就不行了,顺序消费我们要知道下面三要素。
要保证消息有顺序需要保证一下三要素!

  • 消息被发送时保持顺序
  • 消息被存储时保持和发送的顺序一致
  • 消息被消费时保持和存储的顺序一致

至于这里异步顺序消息为什么最后顺序有问题,原因应该是这里异步消息无需同步阻塞等待RocketMQ确认收到消息,所以for循环也不会阻塞等待,然后就把下一条数据发给了RocketMQ,但是数据在网络中传输是存在速度差异的,那么也就导致这100条虽然是有顺序的从我们程序里for循环发出去,没任何阻塞,所以for执行很快,几乎同时100条数据出去,然后100条数据在网络中传输各自有快慢,所以并不是有顺序的被RocketMQ接受所以也就导致消费的时候就是无序的了!
说明
这里我们不管是使用rocker-client还是使用rocketmq-spring-boot-starter发送有序有序消息,其实不管是同步,异步,单向都是可以保证消息送达到一个queue中的,如下!
RocketMQ顺序消息_数据_07
那么消息到达的是同一个queue中,还是出现乱序,那么我们看看消息在queue中的情况!
同步发送

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

        producer.setNamesrvAddr("xxx.xxx.xxx.x:9876");
        //3.启动producer
        producer.start();
  
        //构建消息集合
        List<MsgTest> msgList = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            msgList.add(new MsgTest(100, "我是id为100的第"+(i+1)+"条消息", new Date()));
        }
        //发送消息
        for (int i = 0; i < msgList.size(); i++) {
            Message message = new Message("first-topic-str", "tag1", "i" + i, msgList.get(i).getContext().getBytes());
            /**
             * 参数一:消息对象
             * 参数二:消息队列的选择器
             * 参数三:选择队列的业务标识(订单ID)
             */
            SendResult sendResult = producer.send(message, new MessageQueueSelector() {
                /**
                 *
                 * @param mqs:队列集合
                 * @param msg:消息对象
                 * @param arg:业务标识的参数
                 * @return
                 */
                @Override
                public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                    Integer orderId = (Integer) arg;
                    long index = orderId % mqs.size();
                    return mqs.get((int) index);
                }
            }, msgList.get(i).getId());

            System.out.println("发送结果:" + sendResult);
        }
        producer.shutdown();
    }

生产者发送出去消息结果

RocketMQ顺序消息_i++_08
RockerMQ-Queue接收消息顺序
RocketMQ顺序消息_数据_09
那么消费端消费结果如下
RocketMQ顺序消息_数据_10
那么都是有序的,我们切换成异步发送

异步发送
生产者代码如下

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

        producer.setNamesrvAddr("xxx.xxx.xxx.x:9876");
        //3.启动producer
        producer.start();

        //构建消息集合
        List<MsgTest> msgList = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            msgList.add(new MsgTest(100, "我是id为100的第"+(i+1)+"条消息", new Date()));
        }

        //发送消息
        for (int i=0;i<msgList.size();i++){
            Message message = new Message("first-topic-str", "tag1", "i" + i, msgList.get(i).getContext().getBytes());
            producer.send(message, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                    Integer orderId = (Integer) arg;
                    long index = orderId % mqs.size();
                    return mqs.get((int) index);
                }
            }, msgList.get(i).getId(), new SendCallback() {
                public void onSuccess(SendResult sendResult) {
                    System.out.println("发送结果:" + sendResult);
                }
                public void onException(Throwable e) {
                    System.out.println("发送异常:" + e);
                }
            });
        }

        Thread.sleep(5000);
        producer.shutdown();
    }

生产者异步接收消息发送结果如下
RocketMQ顺序消息_其他_11
到达RocketMQ-queue消息顺序
RocketMQ顺序消息_i++_12
消费者消费消息顺序
RocketMQ顺序消息_i++_13
那么这里即使我们控制异步发送的消息全部到达了同一个queue中,这里使用异步发送是无需等待消息成功到达RocketMQ后才发另一条的,所以无法保证消息到达Queue中的顺序的,这里异步发送消息收到的回调中消息的消息id和Queue中到达的消息顺序不一致,是因为我们收到回调是异步的,由于网络延迟导致异步接收消息投递成功的消息也是无序的,这里单向消息也和这个是同样的道理!所以即使这里我们通过rocket-client手动编写代码完成异步顺序,或者是使用rocketmq-spring-boot-starter调用asyncSendOrderly、sendOneWayOrderly都是无法完成异步、单向顺序消息的!

注意

广播消费模式下不支持顺序消息。
事务支持
RocketMQ顺序消息_ide_14
发送方式支持
RocketMQ顺序消息_数据_15
为了保证先发送的消息先存储到消息队列,必须使用同步发送的方式,否则可能出现先发送的消息后到消息队列中,此时消息就乱序了