RocketMQ笔记(八):顺序消息

发布时间 2023-05-05 08:54:51作者: 无虑的小猪

一、什么是顺序消息

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

  顺序消息是 RocketMQ 提供的一种消息类型,支持消费者按照发送消息的先后顺序获取消息。顺序消息在发送、存储和投递的处理过程中,强调多条消息间的先后顺序关系。RocketMQ 顺序消息的顺序关系通过消息组(MessageGroup)判定和识别,发送顺序消息时需要为每条消息设置归属的消息组,相同消息组的多条消息之间遵循先进先出的顺序关系,不同消息组、无消息组的消息之间不涉及顺序性。

二、顺序消息的特性

  1、消息消费失败或消费超时,会触发服务端重试逻辑,重试消息属于新的消息,原消息的生命周期已结束;

  2、顺序消息消费失败进行消费重试时,为保障消息的顺序性,后续消息不可被消费,必须等待前面的消息消费完成后才能被处理。

  3、顺序消息仅支持使用MessageType为FIFO的主题,即顺序消息只能发送至类型为顺序消息的主题中,发送的消息的类型必须和主题的类型一致。

三、RocketMQ如何保证消息的顺序

  RocketMQ可以严格的保证消息有序,可以分为分区有序或者全局有序。

  RocketMQ在默认的情况下消息发送会采取Round Robin轮询方式把消息发送到不同的queue(分区队列);RocketMQ在消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。

  全局有序:发送和消费参与的queue只有一个,则是全局有序。

  分区有序:多个queue参与,则为分区有序,即相对每个queue,消息都是有序的。

四、RocketMQ保证消息顺序的条件

RocketMQ 的消息的顺序性分为两部分,生产顺序性和消费顺序性。

1、生产顺序性

  RocketMQ 通过生产者和服务端的协议保障单个生产者串行地发送消息,并按序存储和持久化。

1、生产者顺序发送的前提

1.1、单一生产者

  消息生产的顺序性仅支持单一生产者,不同生产者分布在不同的系统,即使设置相同的消息组,不同生产者之间产生的消息也无法判定其先后顺序。

1.2、串行发送

  RocketMQ 生产者客户端支持多线程安全访问,但如果生产者使用多线程并行发送,则不同线程间产生的消息将无法判定其先后顺序。

2、生产者顺序发送

满足上述条件,将顺序消息发送至 RocketMQ 后,会保证设置了同一消息组的消息,按照发送顺序存储在同一队列中。服务端顺序存储逻辑如下:

  ·相同消息组的消息按照先后顺序被存储在同一个队列。

  ·不同消息组的消息可以混合在同一个队列中,且不保证连续。

  

  如上图所示,消息组1和消息组4的消息混合存储在队列1中,RocketMQ 保证消息组1中的消息G1-M1、G1-M2、G1-M3是按发送顺序存储,且消息组4的消息G4-M1、G4-M2也是按顺序存储,但消息组1和消息组4中的消息不涉及顺序关系。

2、消费顺序性

  RocketMQ 通过消费者和服务端的协议保障消息消费严格按照存储的先后顺序来处理。

2.1、投递顺序

  RocketMQ 通过客户端SDK和服务端通信协议保障消息按照服务端存储顺序投递。

  消费者类型为PushConsumer时, RocketMQ 保证消息按照存储顺序一条一条投递给消费者,若消费者类型为SimpleConsumer,则消费者有可能一次拉取多条消息。此时,消息消费的顺序性需要由应用程序保证。

2.2、有限重试

  RocketMQ 顺序消息投递仅在重试次数限定范围内,即一条消息如果一直重试失败,超过最大重试次数后将不再重试,跳过这条消息消费,不会一直阻塞后续消息处理。

  对于严格保证消费顺序的场景,需要合理设置重试次数,避免消息乱序。

3、生产顺序性和消费顺序性组合

  如果消息需要严格按照先进先出(FIFO)的原则处理,即先发送的先消费、后发送的后消费,则必须要同时满足生产顺序性和消费顺序性。

  一般业务场景下,同一个生产者可能对接多个下游消费者,不一定所有的消费者业务都需要顺序消费,可以将生产顺序性和消费顺序性进行差异化组合,应用于不同的业务场景。

生产顺序
消费顺序
顺序性效果
设置消息组,保证消息顺序发送。
顺序消费
按照消息组粒度,严格保证消息顺序。 同一消息组内的消息的消费顺序和发送顺序完全一致。
设置消息组,保证消息顺序发送。
并发消费
并发消费,尽可能按时间顺序处理。
未设置消息组,消息乱序发送。
顺序消费
按队列存储粒度,严格顺序。 基于 Apache RocketMQ 本身队列的属性,消费顺序和队列存储的顺序一致,但不保证和发送顺序一致。
未设置消息组,消息乱序发送。
并发消费
并发消费,尽可能按照时间顺序处理。

五、顺序消息的生命周期

  

1、初始化

  消息被生产者构建并完成初始化,待发送到RocketMQ服务端。

2、待消费

  消息被发送到服务端,等待消费者消费。

3、消费中

  消息被消费者火球,RocketMQ会等待消费者完成消息并提交消费结果,若一定时间后未收到消费者的响应,RocketMQ会对消息进行重试处理。

4、消费提交

  消费者完成消费处理,并向服务端提交消费结果,服务端标记当前消息已被处理(消费成功/失败)。RocketMQ默认支持保留消息,此时消息数据不会被立即删除,逻辑标记已消费。

在消息保存时间到期或存储空间不足被删除前,消费者可以回溯消息重新消费。

5、消息删除

  RocketMQ按照消息保存机制滚动清理最早的消息数据,将消息从物理文件中删除。

六、顺序消息示例demo

  工具类详见:RocketMQ笔记(六):示例代码工具类

1、订单实体

 1 /**
 2  * @Description: 订单信息
 3  */
 4 public class Indent {
 5 
 6     /**
 7      * 订单ID
 8      */
 9     private String orderId;
10 
11     /**
12      * 顺序
13      */
14     private String order;
15 
16     public Indent(String orderId, String order) {
17         this.orderId = orderId;
18         this.order = order;
19     }
20 
21     public String getOrderId() {
22         return orderId;
23     }
24 
25     public void setOrderId(String orderId) {
26         this.orderId = orderId;
27     }
28 
29     public String getOrder() {
30         return order;
31     }
32 
33     public void setOrder(String order) {
34         this.order = order;
35     }
36 }

2、顺序消息生产者

 

 1 import com.snails.rmq.common.RMQConstant;
 2 import com.snails.rmq.utils.ClientUtils;
 3 import com.snails.rmq.utils.DateUtils;
 4 import org.apache.rocketmq.client.producer.DefaultMQProducer;
 5 import org.apache.rocketmq.client.producer.MessageQueueSelector;
 6 import org.apache.rocketmq.client.producer.SendResult;
 7 import org.apache.rocketmq.common.message.Message;
 8 import org.apache.rocketmq.common.message.MessageQueue;
 9 import java.util.ArrayList;
10 import java.util.List;
11 
12 /**
13  * @Description: 发送顺序消息
14  */
15 public class OrderProducer {
16 
17     public static void main(String[] args) throws Exception {
18         // 获取生产者实例
19         DefaultMQProducer producer =
20                 ClientUtils.gainProducerInstance(RMQConstant.ORDER_GROUP, RMQConstant.NAEMSRV_ADDR);
21         // 消息标签
22         String[] tags = new String[]{RMQConstant.MSG_TAG_A, RMQConstant.MSG_TAG_B, RMQConstant.MSG_TAG_C};
23         // 订单列表
24         List<Indent> indentList = OrderProducer.buildIndents();
25 
26         for (int i = 0; i < indentList.size(); i++) {
27             // 获取消息
28             String msgBody = "顺序消息, ".concat(DateUtils.now().concat(":")) + indentList.get(i);
29             Message msg = new Message(RMQConstant.ORDER_TOPIC,
30                     tags[i % tags.length], "RMQKEYS" + i, msgBody.getBytes());
31 
32             // 发送消息
33             SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
34                 @Override
35                 public MessageQueue select(List<MessageQueue> mqList, Message msg, Object obj) {
36                     //根据id选择要发送的消息队列queue
37                     long indentId = (Long) obj;
38                     long index = indentId % mqList.size();
39                     return mqList.get((int) index);
40                 }
41             }, Long.valueOf(indentList.get(i).getOrderId()));
42 
43             // 发送结果打印
44             System.out.printf("发送状态:%s, 消息队列id:%d, 消息内容:%s %n",
45                     sendResult.getSendStatus(),
46                     sendResult.getMessageQueue().getQueueId(),
47                     msgBody);
48         }
49 
50         // 关闭生产实例
51         ClientUtils.shutdownProducer(producer);
52     }
53 
54 
55     /**
56      * 生成模拟订单数据
57      */
58     private static List<Indent> buildIndents() {
59         List<Indent> indentList = new ArrayList<Indent>();
60 
61         Indent indentDemo = new Indent("15103111039", "0");
62         indentList.add(indentDemo);
63 
64         indentDemo = new Indent("15103111065", "1");
65         indentList.add(indentDemo);
66 
67         indentDemo = new Indent("15103111039", "2");
68         indentList.add(indentDemo);
69 
70         indentDemo = new Indent("15103117235", "3");
71         indentList.add(indentDemo);
72 
73         indentDemo = new Indent("15103111065", "4");
74         indentList.add(indentDemo);
75 
76         indentDemo = new Indent("15103117235", "5");
77         indentList.add(indentDemo);
78 
79         indentDemo = new Indent("15103111065", "6");
80         indentList.add(indentDemo);
81 
82         indentDemo = new Indent("15103111039", "7");
83         indentList.add(indentDemo);
84 
85         indentDemo = new Indent("15103117235", "8");
86         indentList.add(indentDemo);
87 
88         indentDemo = new Indent("15103111039", "9");
89         indentList.add(indentDemo);
90 
91         return indentList;
92     }
93 }

 

3、顺序消息消费者

 1 import com.snails.rmq.common.RMQConstant;
 2 import com.snails.rmq.utils.ClientUtils;
 3 import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
 4 import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext;
 5 import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus;
 6 import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly;
 7 import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
 8 import org.apache.rocketmq.common.message.MessageExt;
 9 
10 import java.util.List;
11 import java.util.Random;
12 import java.util.concurrent.TimeUnit;
13 
14 /**
15  * SUCCESS 确认消费
16  * SUSPEND_CURRENT_QUEUE_A_MOMENT  稍后消费
17  *
18  * @Description: 消费顺序消息
19  */
20 public class OrderConsumer {
21 
22     public static void main(String[] args) {
23         // 获取消费者实例
24         String subExpression = RMQConstant.MSG_TAG_A.concat(" || ").concat(RMQConstant.MSG_TAG_B).concat(" || ").concat(RMQConstant.MSG_TAG_C);
25         DefaultMQPushConsumer consumer =
26                 ClientUtils.gainConsumerInstance(RMQConstant.ORDER_GROUP, RMQConstant.NAEMSRV_ADDR, RMQConstant.ORDER_TOPIC, subExpression);
27 
28         /**
29          * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费<br>
30          * 如果非第一次启动,那么按照上次消费的位置继续消费
31          */
32         consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
33 
34         // 注册回调实现类来处理从broker拉取回来的消息
35         consumer.registerMessageListener(new MessageListenerOrderly() {
36 
37             Random random = new Random();
38 
39             @Override
40             public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
41                 context.setAutoCommit(true);
42                 for (MessageExt msg : msgs) {
43                     // 每个queue有唯一的consume线程来消费, 订单对每个queue(分区)有序
44                     System.out.println("consumeThread=" + Thread.currentThread().getName() + ", queueId=" + msg.getQueueId() + ", content:" + new String(msg.getBody()));
45                     try {
46                         // TODO 业务逻辑处理
47                         TimeUnit.SECONDS.sleep(random.nextInt(10));
48                     } catch (Exception e) {
49                         e.printStackTrace();
50                     }
51                 }
52 
53                 return ConsumeOrderlyStatus.SUCCESS;
54             }
55         });
56 
57         // 启动消费者实例
58         ClientUtils.startupConsumer(consumer);
59     }
60 }