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