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 作为替代。
关于作者:
王昕(QQ:475660)
在广州工作生活30余年。十多年开发经验,在Java、即时通讯、NoSQL、BPM、大数据等领域较有经验。
目前维护的开源产品:https://gitee.com/475660
目前维护的开源产品:https://gitee.com/475660