RocketMQ 顺序消息解析——图解、源码级解析

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: RocketMQ 顺序消息解析——图解、源码级解析

# 顺序消息

说到<font color = "#33aaff">顺序</font>,我们经常会将它和现实里的时间关联起来,即按照时间顺序表示事件的先后关系。比如发生在10:00的消息A就要早于发生在11:00的消息B。


上面例子之所以成立的原因是他们有相同的参考系,倘若A的时间是北京时间,而B的时间是纽约时间,这个先后顺序就不一定成立了。


<br/>


当然除了时间以外,A和B之间的因果关系也可以断定他们的顺序,例如退款一定发生于付款之后。


综上所述,我们所讲的<font color = "#33aaff">顺序</font>,实际上的意思是:

1. 有统一的时间参考下,事件发生的先后关系

2. 没有统一的时间参考下的happen-before关系


<br/>


## 分布式环境下的顺序

设想一个分布式的环境:


1. 同一线程上的事件是有明确顺序关系的,发生的先后顺序就是

2. 不同线程的事件只能通过因果关系去推断


![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/56b3620371ac4b248595a60e7d0ef5c6.png#pic_center)

例如针对上图的两个线程A和B,进程A中的事件有明显的先后顺序(A1 -> A2 -> A3 -> A4),又因为A1给B2发了消息,所以A1一定在B2之前……


<br/>


## 消息中间件中的顺序消息

RocketMQ支持顺序消息的功能,既有<font color="#ffaa33">顺序发送</font>又有<font color="#ffaa33">顺序消费</font>。


而顺序消息又包含了两种类型:

- **分区顺序**:一个Partition内所有的消息按照先进先出的顺序进行发布和消费

- **全局顺序**:一个Topic内所有的消息按照先进先出的顺序进行发布和消费


对于顺序消费,需要明确哪些来自同一个发送线程的消息在消费时是按照相同的发送顺序来进行消费的。


<br/>


**在MQ里,顺序在不同的阶段里都需要得到保障:**

1. 发送消息是保证顺序


在同一个线程内应该采取同步的方式发送;


2. 消息按照顺序存储


按照A、B顺序发送的消息,在空间上A也要保存在B之前;


3. 按照存储的顺序消费消息


消息A、B到达后,Consumer先消费A后消费B


![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/10b6929d82124ec8b7c08c453492ad70.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5bCP546L5pu-5piv5bCR5bm0,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center)

如上图所示,假设有两个订单A、B,消息的原始数据为a1、b1、b2、a2、a3、b3


- **发送阶段:** A订单的消息要保证a1、a2、a3的顺序,B订单的消息要保证b1、b2、b3的顺序,但是两个订单之间的消息没有先后顺序要求,所以可以由两个线程分别发送


- **存储阶段:**  A订单的消息要保证a1、a2、a3的顺序,B订单的消息要保证b1、b2、b3的顺序,但是两个订单之间的消息没有先后顺序要求


- **消费阶段:** 可以由一个线程按照接收到的顺序进行消费,也可以用两个线程分别消费订单A和订单B的数据


<br/><br/>


# RocketMQ中顺序消息的实现

在RocketMQ里顺序消息的实现如下图所示:

![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/f389280a784e44989a2ab7f11576bd25.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5bCP546L5pu-5piv5bCR5bm0,size_19,color_FFFFFF,t_70,g_se,x_16#pic_center)

假设创建订单,订单付款,完成订单是三个必须的顺序消息,通过他们相同的订单ID将其路由到不同的分区中,Consumer消费时一个分区只对应一个线程来消费,从而保障消息的顺序性。


<br/><br/>

# Producer顺序发送

Producer要确保消息有序性唯一要做的就是将消息路由到特定的分区,在RocketMQ中,通过`MessageQueueSelector`来实现分区的选择。


```java

public interface MessageQueueSelector {

   MessageQueue select(final List<MessageQueue> mqs, final Message msg, final Object arg);

}

```


1. `List<MessageQueue> mqs`:消息要发送到的Topic下的所有队列

2. `Message msg`:消息对象

3. `Object arg`:用户自定义的参数


例如下面的代码就可以将相同订单ID的消息路由到相同的分区:


```java

long orderId = order.getOrderId;

return mqs.get(orderId % mqs.size());

```

**完整的示例Demo如下:**

```java

public class Main {

   public static void main(String[] args) throws Exception {

       DefaultMQProducer producer = new DefaultMQProducer("Group A");

       producer.setNamesrvAddr("localhost");

       producer.start();


       for (int i = 0; i < 100; i++) {

           // 流水号

           int orderId = i % 10;

           // 构造消息对象

           Message msg = new Message("Topic A", "TagA", ("Hello RocketMQ " + i).getBytes(StandardCharsets.UTF_8));

           // 发送消息,将相同订单id的消息路由到同一个MQ里

           SendResult sendResult = producer.send(msg, new MessageQueueSelector() {

               @Override

               public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {

                   Integer id = (Integer) arg;

                   int index = id % mqs.size();

                   return mqs.get(index);

               }

           }, orderId);

           System.out.println(sendResult);

       }

       producer.shutdown();

   }

}

```

<br/><br/>

# Consumer顺序消费

RocketMQ消费消息有两种形式:拉模式和推模式,分别对应`MQPullConsumer`和`MQPushConsumer`。


- `MQPullConsumer`由用户线程控制,主动从服务端获取MQ中的一条消息,所以拿到的消息也是天然有顺序的,Consumer在消费时也要保证自己的消费顺序

![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/26523021c33d4fd98f39becfde62bd20.png#pic_center)


- `MQPushConsumer`由消息中间件主动推送消息给Consumer,由用户注册`MessageListener`来消费消息


<br/>


**以`MQPullConsumer`为例,保证消息顺序的流程如下:**

1. `PullMessageService`以单线程从Broker中拿消息

2. 拿到消息后将其放入`ProcessQueue`中(可以看做是消息的缓存)

3. `ConsumeMessageService`以多线程的形式尝试获取锁,拿到锁之后再从`ProcessQueue`中获取消息


**示例代码如下:**


```java

public class Consumer {


   public static void main(String[] args) throws MQClientException {

       DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("Group A");

       consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);

       consumer.subscribe("Topic A", "Tag A");

       consumer.setNamesrvAddr("localhost");

     

       consumer.registerMessageListener(new MessageListenerOrderly() {

           @Override

           public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {

               System.out.println(Thread.currentThread().getName() + " Receive New Messages: " + msgs);

               return ConsumeOrderlyStatus.SUCCESS;

           }

       });

     

       consumer.start();

       System.out.println("Consumer Started.");

   }

}

```


**如何保证消费消息的顺序?**

1. 消息添加至`ProcessQueue`的过程是单线程执行的,所以`ProcessQueue`中的消息也是顺序的

2. 消费时是从`ProcessQueue`中读取消息进行消费,并且使用锁进行了并发控制,所以也是有序的



# 顺序消息带来的缺陷

聊完了什么是顺序消息以及他们的实现方式,接下来就看一看为了保证消息的一致,系统所引入的一些缺陷。


1. 当出现热点数据时,可能某些`MessageQueue`的数据量会很大

2. 发送顺序消息不能使用分布式系统的容错性,因为针对同一条数据只能被发送到某一个`MessageQueue`里

3. 某一串顺序消息里,即使有某条消息消费失败,也不能跳过



**一些尝试性的解决方案:**

目前对于热点数据没有什么好的解决方法,只能通过优化路由策略或拆分`MessageQueue`来将消息尽可能均匀地发送给不同的`MessageQueue`。


对于同一个`MessageQueue`,也可以有其副本,这些`MessageQueue`之间有自己的路由规则。


对于消费失败的消息,可以提供重试机制来重新消费这条消息,前提是要满足系统的幂等性。

相关实践学习
消息队列RocketMQ版:基础消息收发功能体验
本实验场景介绍消息队列RocketMQ版的基础消息收发功能,涵盖实例创建、Topic、Group资源创建以及消息收发体验等基础功能模块。
消息队列 MNS 入门课程
1、消息队列MNS简介 本节课介绍消息队列的MNS的基础概念 2、消息队列MNS特性 本节课介绍消息队列的MNS的主要特性 3、MNS的最佳实践及场景应用 本节课介绍消息队列的MNS的最佳实践及场景应用案例 4、手把手系列:消息队列MNS实操讲 本节课介绍消息队列的MNS的实际操作演示 5、动手实验:基于MNS,0基础轻松构建 Web Client 本节课带您一起基于MNS,0基础轻松构建 Web Client
相关文章
|
3天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
14 2
|
1月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
66 0
|
1月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
52 0
|
1月前
|
存储 Java C++
Collection-PriorityQueue源码解析
Collection-PriorityQueue源码解析
60 0
|
1月前
|
安全 Java 程序员
Collection-Stack&Queue源码解析
Collection-Stack&Queue源码解析
80 0
|
3天前
|
存储 安全 Linux
Golang的GMP调度模型与源码解析
【11月更文挑战第11天】GMP 调度模型是 Go 语言运行时系统的核心部分,用于高效管理和调度大量协程(goroutine)。它通过少量的操作系统线程(M)和逻辑处理器(P)来调度大量的轻量级协程(G),从而实现高性能的并发处理。GMP 模型通过本地队列和全局队列来减少锁竞争,提高调度效率。在 Go 源码中,`runtime.h` 文件定义了关键数据结构,`schedule()` 和 `findrunnable()` 函数实现了核心调度逻辑。通过深入研究 GMP 模型,可以更好地理解 Go 语言的并发机制。
|
16天前
|
消息中间件 缓存 安全
Future与FutureTask源码解析,接口阻塞问题及解决方案
【11月更文挑战第5天】在Java开发中,多线程编程是提高系统并发性能和资源利用率的重要手段。然而,多线程编程也带来了诸如线程安全、死锁、接口阻塞等一系列复杂问题。本文将深度剖析多线程优化技巧、Future与FutureTask的源码、接口阻塞问题及解决方案,并通过具体业务场景和Java代码示例进行实战演示。
36 3
|
1月前
|
存储
让星星⭐月亮告诉你,HashMap的put方法源码解析及其中两种会触发扩容的场景(足够详尽,有问题欢迎指正~)
`HashMap`的`put`方法通过调用`putVal`实现,主要涉及两个场景下的扩容操作:1. 初始化时,链表数组的初始容量设为16,阈值设为12;2. 当存储的元素个数超过阈值时,链表数组的容量和阈值均翻倍。`putVal`方法处理键值对的插入,包括链表和红黑树的转换,确保高效的数据存取。
53 5
|
1月前
|
Java Spring
Spring底层架构源码解析(三)
Spring底层架构源码解析(三)
108 5
|
1月前
|
XML Java 数据格式
Spring底层架构源码解析(二)
Spring底层架构源码解析(二)

推荐镜像

更多