如何保证rocketmq消费顺序


问题 : 我们知道消息队列可以在高并发的情况下,实现 削峰填谷,以及可以 异步解偶。但是 某些业务场景下,我们需要 保证消息是严格按照一定顺序 去消费的,这时候我们要怎么办? 以rocketmq为例。

我们 知道 ,消息从 producer 发送到 broker 队列中 ,一般是 轮训发送到 多个broker中的,而 consumer 消费拉取的时候 一般都是 同时拉取 多个queue的消息消费,这样就容易导致 ,消费的无序性,如下图

springboot rockmq 消费 rocketmq消费策略_System

springboot rockmq 消费 rocketmq消费策略_System_02

举例 : 电商平台中 订单的创建 ,支付,推送 都需要按照严格的顺序消费,那么为了保证消费顺序 我们如何操作? 

解决 : 1 我们要保证 producer发送消息时 需要 按顺序发送 ,并且发送到 同一个队列 ,这里我们可以使用 业务字段 id号或者 订单号 按队列数 取模的方法 ,通过MessageQueueSelector 来保证 同一个订单号的 消息,按顺序 发送到 同一个queue.

             2 我们要保证 consumer 按顺序 消费消息 ,rocketmq中 MessageListenerConcurrently 是无法确保消息消费顺序的,只有 MessageListenerOrderly 才可以保证消费顺序。

代码如下 :

rocketmq消息生产端示例代码如下:

/**
  * Producer,发送顺序消息
  */
 public class Producer {
     
     public static void main(String[] args) throws IOException {
         try {
             DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
  
             producer.setNamesrvAddr("10.11.11.11:9876;10.11.11.12:9876");
  
             producer.start();
  
             String[] tags = new String[] { "TagA", "TagC", "TagD" };
             
             // 订单列表
             List<OrderDemo> 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("TopicTestjjj", 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;
                         long index = id % mqs.size();
                         return mqs.get((int)index);
                     }
                 }, orderList.get(i).getOrderId());//订单id
  
                 System.out.println(sendResult + ", body:" + body);
             }
             
             producer.shutdown();
  
         } catch (MQClientException e) {
             e.printStackTrace();
         } catch (RemotingException e) {
             e.printStackTrace();
         } catch (MQBrokerException e) {
             e.printStackTrace();
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.in.read();
     }
     
     /**
      * 生成模拟订单数据 
      */
     private List<OrderDemo> buildOrders() {
         List<OrderDemo> orderList = new ArrayList<OrderDemo>();
  
         OrderDemo orderDemo = new OrderDemo();
         orderDemo.setOrderId(15103111039L);
         orderDemo.setDesc("创建");
         orderList.add(orderDemo);
         
         orderDemo = new OrderDemo();
         orderDemo.setOrderId(15103111065L);
         orderDemo.setDesc("创建");
         orderList.add(orderDemo);
         
         orderDemo = new OrderDemo();
         orderDemo.setOrderId(15103111039L);
         orderDemo.setDesc("付款");
         orderList.add(orderDemo);
         
         orderDemo = new OrderDemo();
         orderDemo.setOrderId(15103117235L);
         orderDemo.setDesc("创建");
         orderList.add(orderDemo);
         
         orderDemo = new OrderDemo();
         orderDemo.setOrderId(15103111065L);
         orderDemo.setDesc("付款");
         orderList.add(orderDemo);
         
         orderDemo = new OrderDemo();
         orderDemo.setOrderId(15103117235L);
         orderDemo.setDesc("付款");
         orderList.add(orderDemo);
         
         orderDemo = new OrderDemo();
         orderDemo.setOrderId(15103111065L);
         orderDemo.setDesc("完成");
         orderList.add(orderDemo);
         
         orderDemo = new OrderDemo();
         orderDemo.setOrderId(15103111039L);
         orderDemo.setDesc("推送");
         orderList.add(orderDemo);
         
         orderDemo = new OrderDemo();
         orderDemo.setOrderId(15103117235L);
         orderDemo.setDesc("完成");
         orderList.add(orderDemo);
         
         orderDemo = new OrderDemo();
         orderDemo.setOrderId(15103111039L);
         orderDemo.setDesc("完成");
         orderList.add(orderDemo);
         
         return orderList;
     }


输出:

从图中红色框可以看出,orderId等于15103111039的订单被顺序放入queueId等于7的队列。queueOffset同时在顺序增长。

发送时有序,接收(消费)时也要有序,才能保证顺序消费。如下这段代码演示了普通消费(非有序消费)的实现方式。

/**
  * 普通消息消费
  */
 public class Consumer {
  
     public static void main(String[] args) throws MQClientException {
         DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3");
         consumer.setNamesrvAddr("10.11.11.11:9876;10.11.11.12:9876");
         /**
          * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费<br>
          * 如果非第一次启动,那么按照上次消费的位置继续消费
          */
         consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
  
         consumer.subscribe("TopicTestjjj", "TagA || TagC || TagD");
  
         consumer.registerMessageListener(new MessageListenerConcurrently() {
  
             Random random = new Random();
  
             @Override
             public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                 System.out.print(Thread.currentThread().getName() + " Receive New Messages: " );
                 for (MessageExt msg: msgs) {
                     System.out.println(msg + ", content:" + new String(msg.getBody()));
                 }
                 try {
                     //模拟业务逻辑处理中...
                     TimeUnit.SECONDS.sleep(random.nextInt(10));
                 } catch (Exception e) {
                     e.printStackTrace();
                 }
                 return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
             }
         });
  
         consumer.start();
  
         System.out.println("Consumer Started.");
     }
 }

输出:

可见,订单号为15103111039的订单被消费时顺序完成乱了。所以用MessageListenerConcurrently这种消费者是无法做到顺序消费的,采用下面这种方式就做到了顺序消费:

/**
  * 顺序消息消费,带事务方式(应用可控制Offset什么时候提交)
  */
 public class ConsumerInOrder {
  
     public static void main(String[] args) throws MQClientException {
         DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3");
         consumer.setNamesrvAddr("10.11.11.11:9876;10.11.11.12:9876");
         /**
          * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费<br>
          * 如果非第一次启动,那么按照上次消费的位置继续消费
          */
         consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
  
         consumer.subscribe("TopicTestjjj", "TagA || TagC || TagD");
  
         consumer.registerMessageListener(new MessageListenerOrderly() {
  
             Random random = new Random();
  
             @Override
             public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                 context.setAutoCommit(true);
                 System.out.print(Thread.currentThread().getName() + " Receive New Messages: " );
                 for (MessageExt msg: msgs) {
                     System.out.println(msg + ", 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.");
     }
 }


输出:

MessageListenerOrderly能够保证顺序消费,从图中我们也看到了期望的结果。图中的输出是只启动了一个消费者时的输出,看起来订单号还是混在一起,但是每组订单号之间是有序的。因为消息发送时被分配到了三个队列(参见前面生产者输出日志),那么这三个队列的消息被这唯一消费者消费。

如果启动2个消费者呢?那么其中一个消费者对应消费2个队列,另一个消费者对应消费剩下的1个队列。

如果启动3个消费者呢?那么每个消费者都对应消费1个队列,订单号就区分开了。输出变为这样:

消费者1输出:

消费者2输出:

消费者3输出:

很完美,有木有?!

按照这个示例,把订单号取了做了一个取模运算再丢到selector中,selector保证同一个模的都会投递到同一条queue。即: 相同订单号的--->有相同的模--->有相同的queue。最后就会类似这样:

总结:

rocketmq的顺序消息需要满足2点:

1.Producer端保证发送消息有序,且发送到同一个队列。
2.consumer端保证消费同一个队列。