RocketMQ——顺序消息

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

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

之所以出现你这个场景看起来不是顺序的,是因为发送消息的时候,消息发送默认是会采用轮询的方式发送到不通的queue(分区)。如图:


而消费端消费的时候,是会分配到多个queue的,多个queue是同时拉取提交消费。如图:


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

下面用订单进行示例。一个订单的顺序流程是:创建、付款、推送、完成。订单号相同的消息会被先后发送到同一个队列中,消费时,同一个OrderId获取到的肯定是同一个队列。

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

[java]  view plain  copy
  1. /** 
  2.  * Producer,发送顺序消息 
  3.  */  
  4. public class Producer {  
  5.       
  6.     public static void main(String[] args) throws IOException {  
  7.         try {  
  8.             DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");  
  9.    
  10.             producer.setNamesrvAddr("10.11.11.11:9876;10.11.11.12:9876");  
  11.    
  12.             producer.start();  
  13.    
  14.             String[] tags = new String[] { "TagA""TagC""TagD" };  
  15.               
  16.             // 订单列表  
  17.             List<OrderDemo> orderList =  new Producer().buildOrders();  
  18.               
  19.             Date date = new Date();  
  20.             SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
  21.             String dateStr = sdf.format(date);  
  22.             for (int i = 0; i < 10; i++) {  
  23.                 // 加个时间后缀  
  24.                 String body = dateStr + " Hello RocketMQ " + orderList.get(i);  
  25.                 Message msg = new Message("TopicTestjjj", tags[i % tags.length], "KEY" + i, body.getBytes());  
  26.    
  27.                 SendResult sendResult = producer.send(msg, new MessageQueueSelector() {  
  28.                     @Override  
  29.                     public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {  
  30.                         Long id = (Long) arg;  
  31.                         long index = id % mqs.size();  
  32.                         return mqs.get((int)index);  
  33.                     }  
  34.                 }, orderList.get(i).getOrderId());//订单id  
  35.    
  36.                 System.out.println(sendResult + ", body:" + body);  
  37.             }  
  38.               
  39.             producer.shutdown();  
  40.   
  41.         } catch (MQClientException e) {  
  42.             e.printStackTrace();  
  43.         } catch (RemotingException e) {  
  44.             e.printStackTrace();  
  45.         } catch (MQBrokerException e) {  
  46.             e.printStackTrace();  
  47.         } catch (InterruptedException e) {  
  48.             e.printStackTrace();  
  49.         }  
  50.         System.in.read();  
  51.     }  
  52.       
  53.     /** 
  54.      * 生成模拟订单数据  
  55.      */  
  56.     private List<OrderDemo> buildOrders() {  
  57.         List<OrderDemo> orderList = new ArrayList<OrderDemo>();  
  58.   
  59.         OrderDemo orderDemo = new OrderDemo();  
  60.         orderDemo.setOrderId(15103111039L);  
  61.         orderDemo.setDesc("创建");  
  62.         orderList.add(orderDemo);  
  63.           
  64.         orderDemo = new OrderDemo();  
  65.         orderDemo.setOrderId(15103111065L);  
  66.         orderDemo.setDesc("创建");  
  67.         orderList.add(orderDemo);  
  68.           
  69.         orderDemo = new OrderDemo();  
  70.         orderDemo.setOrderId(15103111039L);  
  71.         orderDemo.setDesc("付款");  
  72.         orderList.add(orderDemo);  
  73.           
  74.         orderDemo = new OrderDemo();  
  75.         orderDemo.setOrderId(15103117235L);  
  76.         orderDemo.setDesc("创建");  
  77.         orderList.add(orderDemo);  
  78.           
  79.         orderDemo = new OrderDemo();  
  80.         orderDemo.setOrderId(15103111065L);  
  81.         orderDemo.setDesc("付款");  
  82.         orderList.add(orderDemo);  
  83.           
  84.         orderDemo = new OrderDemo();  
  85.         orderDemo.setOrderId(15103117235L);  
  86.         orderDemo.setDesc("付款");  
  87.         orderList.add(orderDemo);  
  88.           
  89.         orderDemo = new OrderDemo();  
  90.         orderDemo.setOrderId(15103111065L);  
  91.         orderDemo.setDesc("完成");  
  92.         orderList.add(orderDemo);  
  93.           
  94.         orderDemo = new OrderDemo();  
  95.         orderDemo.setOrderId(15103111039L);  
  96.         orderDemo.setDesc("推送");  
  97.         orderList.add(orderDemo);  
  98.           
  99.         orderDemo = new OrderDemo();  
  100.         orderDemo.setOrderId(15103117235L);  
  101.         orderDemo.setDesc("完成");  
  102.         orderList.add(orderDemo);  
  103.           
  104.         orderDemo = new OrderDemo();  
  105.         orderDemo.setOrderId(15103111039L);  
  106.         orderDemo.setDesc("完成");  
  107.         orderList.add(orderDemo);  
  108.           
  109.         return orderList;  
  110.     }  

输出:


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

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

[java]  view plain  copy
  1. /** 
  2.  * 普通消息消费 
  3.  */  
  4. public class Consumer {  
  5.    
  6.     public static void main(String[] args) throws MQClientException {  
  7.         DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3");  
  8.         consumer.setNamesrvAddr("<span style="color:rgb(47,47,47);font-size:14px;line-height:27.2px;font-family:Arial, Helvetica, sans-serif;">10.11.11.11:9876;10.11.11.12:9876</span>");  
  9.         /** 
  10.          * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费<br> 
  11.          * 如果非第一次启动,那么按照上次消费的位置继续消费 
  12.          */  
  13.         consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);  
  14.    
  15.         consumer.subscribe("TopicTestjjj""TagA || TagC || TagD");  
  16.    
  17.         consumer.registerMessageListener(new MessageListenerConcurrently() {  
  18.    
  19.             Random random = new Random();  
  20.    
  21.             @Override  
  22.             public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {  
  23.                 System.out.print(Thread.currentThread().getName() + " Receive New Messages: " );  
  24.                 for (MessageExt msg: msgs) {  
  25.                     System.out.println(msg + ", content:" + new String(msg.getBody()));  
  26.                 }  
  27.                 try {  
  28.                     //模拟业务逻辑处理中...  
  29.                     TimeUnit.SECONDS.sleep(random.nextInt(10));  
  30.                 } catch (Exception e) {  
  31.                     e.printStackTrace();  
  32.                 }  
  33.                 return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;  
  34.             }  
  35.         });  
  36.    
  37.         consumer.start();  
  38.    
  39.         System.out.println("Consumer Started.");  
  40.     }  
  41. }  

输出:


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

[java]  view plain  copy
  1. /** 
  2.  * 顺序消息消费,带事务方式(应用可控制Offset什么时候提交) 
  3.  */  
  4. public class ConsumerInOrder {  
  5.    
  6.     public static void main(String[] args) throws MQClientException {  
  7.         DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3");  
  8.         consumer.setNamesrvAddr("<span style="color:rgb(47,47,47);font-size:14px;line-height:27.2px;font-family:Arial, Helvetica, sans-serif;">10.11.11.11:9876;10.11.11.12:9876</span>");  
  9.         /** 
  10.          * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费<br> 
  11.          * 如果非第一次启动,那么按照上次消费的位置继续消费 
  12.          */  
  13.         consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);  
  14.    
  15.         consumer.subscribe("TopicTestjjj""TagA || TagC || TagD");  
  16.    
  17.         consumer.registerMessageListener(new MessageListenerOrderly() {  
  18.    
  19.             Random random = new Random();  
  20.    
  21.             @Override  
  22.             public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {  
  23.                 context.setAutoCommit(true);  
  24.                 System.out.print(Thread.currentThread().getName() + " Receive New Messages: " );  
  25.                 for (MessageExt msg: msgs) {  
  26.                     System.out.println(msg + ", content:" + new String(msg.getBody()));  
  27.                 }  
  28.                 try {  
  29.                     //模拟业务逻辑处理中...  
  30.                     TimeUnit.SECONDS.sleep(random.nextInt(10));  
  31.                 } catch (Exception e) {  
  32.                     e.printStackTrace();  
  33.                 }  
  34.                 return ConsumeOrderlyStatus.SUCCESS;  
  35.             }  
  36.         });  
  37.    
  38.         consumer.start();  
  39.    
  40.         System.out.println("Consumer Started.");  
  41.     }  
  42. }  
输出:


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

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

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

消费者1输出:


消费者2输出:


消费者3输出:


很完美,有木有?!

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


总结:

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

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

部分内容图片引用自https://www.zhihu.com/question/30195969

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值