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