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

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 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
相关文章
|
2月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
87 2
|
3月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
87 0
|
3月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
68 0
|
10天前
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
|
10天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
10天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是"将对象的创建与使用分离”。这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。创建型模式分为5种:单例模式、工厂方法模式抽象工厂式、原型模式、建造者模式。
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
2月前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
57 12
|
29天前
|
PyTorch Shell API
Ascend Extension for PyTorch的源码解析
本文介绍了Ascend对PyTorch代码的适配过程,包括源码下载、编译步骤及常见问题,详细解析了torch-npu编译后的文件结构和三种实现昇腾NPU算子调用的方式:通过torch的register方式、定义算子方式和API重定向映射方式。这对于开发者理解和使用Ascend平台上的PyTorch具有重要指导意义。
|
2月前
|
消息中间件 存储 监控
深度写作:深入源码理解MQ长轮询优化机制
【11月更文挑战第22天】在分布式系统中,消息队列(Message Queue, MQ)扮演着至关重要的角色。MQ不仅实现了应用间的解耦,还提供了异步消息处理、流量削峰等功能。而在MQ的众多特性中,长轮询(Long Polling)机制因其能有效提升消息处理的实时性和效率,备受关注。
75 12
|
10天前
|
安全 搜索推荐 数据挖掘
陪玩系统源码开发流程解析,成品陪玩系统源码的优点
我们自主开发的多客陪玩系统源码,整合了市面上主流陪玩APP功能,支持二次开发。该系统适用于线上游戏陪玩、语音视频聊天、心理咨询等场景,提供用户注册管理、陪玩者资料库、预约匹配、实时通讯、支付结算、安全隐私保护、客户服务及数据分析等功能,打造综合性社交平台。随着互联网技术发展,陪玩系统正成为游戏爱好者的新宠,改变游戏体验并带来新的商业模式。

推荐镜像

更多