RocketMq详细总结

557 阅读6分钟

RocketMq详细总结

核心架构

Untitled.png

在 RocketMQ 主要的组件如下。

NameServer

Broker

NameServer 集群,Topic 的路由注册中心,为客户端根据 Topic 提供路由服务,从而引导客户端向 Broker 发送消息。NameServer 之间的节点不通信。每一个NameServer内部维护着所有的broker的地址列表,所有topic和topic对一个的路由信息。消息生产者在发送消息之前先与任意一台 NameServer 建立连接,获取 Broker 服务器的地址列表,然后根据负载均衡算法从列表中选择一台消息服务器发送消息。 路由信息在 NameServer 集群中数据一致性采取的最终一致性。

  • Broker内部维护着一个个Message Queue,用来存储消息的索引,真正存储消息的地方是CommitLog(日志文件)。

消息存储服务器,分为两种角色:Master 与 Slave。消息存储和转发。 上图中呈现的就是 2 主 2 从的部署架构,在 RocketMQ 中,主服务承担读写操作,从服务器作为一个备份,当主服务器存在压力时,从服务6器可以承担读服务(消息消费)。所有 Broker,包含 Sftlave 服务器每隔 30s 会向 NameServer 发送心跳包,心跳包中会包含存在在 Broker 上所有的 Topic 的路由信息。

  • 单个Broker与所有的Nameserver保持着长连接和心跳,并会定时将Topic信息同步到NameServer,和NameServer的通信底层是通过Netty实现的
  • Broker 的 Master 与 Slave 间的数据同步方式,分为同步
  • 复制与异步复制。由于异步复制、异步刷盘可能会丢失少量消息,因此 Broker 默认采用的是同步双写的方式,消息写入 master 成功后,master 会等待 slave 同步数据成功后才向 Producer 返回成功 ACK ,即 Master 与 Slave 都要写入成功后才会返回成功 ACK 。这样可以保证消息发送时消息不丢失。副本组中,各个节点处理的速度不同,也就有了日志水位的概念 (高水位对消费者不可见)。在 master 宕机时,同步副本集中的其余节点会自动选举出新的 master 代替工作(Raft 协议)。

Client

消息客户端,包括 Producer(消息发送者)和 Consumer(消费消费者)。客户端在同一时间只会连接一台 NameServer,只有在连接出现异常时才会向尝试连接另外一台。客户端每隔 30s 向 NameServer 发起 Topic 的路由信息查询。

温馨提示:NameServer 是在内存中存储 Topic 的路由信息,持久化 Topic 路由信息的地方是在 Broker 中,即 ${ROCKETMQ_HOME}/store/config/topics.json。

在 RocketMQ 4.5.0 版本后引入了多副本机制,即一个复制组(m-s)可以演变为基于 Raft 协议的复制组,复制组内部使用 Raft 协议保证 Broker 节点数据的强一致性,该部署架构在金融行业用的比较多。

Producer,消息生产者,与 NameServer 随机一个节点建立长连接,定时从 NameServer 获取 topic 路由信息,与 master broker 建立长连接,定时发送心跳,Producer 只与 master 建立连接产生通信,不与 slave 建立连接。生产者和消费者都有组(Group)的概念,同一组节点的生产/消费逻辑相同。

Consumer,消息消费者,与 NameServer 随机一个节点建立长连接,定时从 NameServer 获取 topic 的路由信息,并获取想要消费的 queue 。可以和提供服务的 master 或 slave 建立长连接,定时向 master 和 slave 发送心跳,既可以从 master 订阅消息,也可以从 slave 订阅消息

那这三个消费者如何来分工来共同消费 order_topic 中的消息呢?

在 RocketMQ 中支持广播模式与集群模式。

  • 广播模式:一个消费组内的所有消费者每一个都会处理 Topic 中的每一条消息,通常用于刷新内存缓存。
  • 集群模式:一个消费组内的所有消费者共同消费一个 Topic 中的消息,即分工协作,一个消费者消费一部分数据,启动负载均衡。

RocketMq消息队列的特性

  • 订阅和发布
  • 消息顺序
  • 事务消息
  • 消息过滤(tag,sql等语法)
  • 消息可靠性
  • 定时消息
  • 消息重试
  • 消息回溯

消息模式(pull or Push)

cketMQ消息订阅有两种模式,一种是Push模式(MQPushConsumer),即MQServer主动向 消费端推送;另外一种是Pull模式(MQPullConsumer),即消费端在需要时,主动到MQ Server拉 取。但在具体实现时,Push和Pull模式本质都是采用消费端主动拉取的方式,即consumer轮询从 broker拉取消息。

Push方式里,consumer把长轮询的动作封装了,并注册MessageListener监听器,取到消息后, 唤醒MessageListener的consumeMessage()来消费,对用户而言,感觉消息是被推送过来的。 Pull方式里,取消息的过程需要用户自己主动调用,首先通过打算消费的Topic拿到 MessageQueue的集合,遍历MessageQueue集合,然后针对每个MessageQueue批量取消息,一次 取完后,记录该队列下一次要取的开始offset,直到取完了,再换另一个MessageQueue。 RocketMQ使用长轮询机制来模拟Push效果,算是兼顾了二者的优点

提高写入性能

另一种提高发送速度的方法是增加Producer的并发量,使用多个Producer同时发送,

我们不用担心多Producer同时写会降低消息写磁盘的效率,RocketMQ引入了一个并发窗口,在窗 口内消息可以并发地写入DirectMem中,然后异步地将连续一段无空洞的数据刷入文件系统当中。 顺序写CommitLog可让RocketMQ无论在HDD还是SSD磁盘情况下都能保持较高的写入性能。

消息存储

RocketMQ消息的存储是由ConsumeQueue和CommitLog配合完成 的,消息真正的物理存储文件 是CommitLog,ConsumeQueue是消息的逻辑队列,类似数据库的索引文件,存储的是指向物理存储 的地址

F

消费模型

RocketMQ 提供了并发消费、顺序消费两种消费模型。

  • 并发消费:对一个队列中消息,每一个消费者内部都会创建一个线程池,对队列中的消息多线程处理,即偏移量大的消息比偏移量小的消息有可能先消费。
  • 顺序消费:在某一项场景,例如 MySQL binlog 场景,需要消息按顺序进行消费。在 RocketMQ 中提供了基于队列的顺序消费模型,即尽管一个消费组中的消费者会创建一个多线程,但针对同一个 Queue,会加锁。

温馨提示:并发消费模型中,消息消费失败默认会重试 16 次,每一次的间隔时间不一样;而顺序消费,如果一条消息消费失败,则会一直消费,直到消费成功。故在顺序消费的使用过程中,应用程序需要区分系统异常、业务异常,如果是不符合业务规则导致的异常,则重试多少次都无法消费成功,这个时候一定要告警机制,及时进行人为干预,否则消费会积压。

Page Cache

由内存中的物理page组成,其内容对应磁盘上的block。 page cache的大小是动态变化的。 backing store: cache缓存的存储设备 一个page通常包含多个block, 而block不一定是连续的。

负载均衡

rocketMQ的负载均衡,都在client端完成。

时间轮算法

为了解决定时任务队列遍历任务导致的性能开销,RocketMQ 5.0 定时消息引入了秒级的时间轮算法。如下图:

时间轮 是一种 实现延迟功能(定时器)巧妙算法。如果一个系统存在大量的任务调度,时间轮可以高效的利用线程资源来进行批量化调度。把大批量的调度任务全部都绑定时间轮上,通过时间轮进行所有任务的管理,触发以及运行。能够高效地管理各种延时任务,周期任务,通知任务等。

时间轮(TimingWheel)是一个 存储定时任务的环形队列,底层采用数组实现,数组中的每个元素可以存放一个定时任务列表(TimerTaskList) 。TimerTaskList 是一个环形的双向链表,链表中的每一项表示的都是定时任务项(TimerTaskEntry),其中封装了真正的定时任务 TimerTask。

时间轮算法的优势是不用去遍历所有的任务,每一个时间节点上的任务用链表串起来,当时间轮上的指针移动到当前的时间时,这个时间节点上的全部任务都执行。

虽然上面只是一个 60s 的时间轮,但是对于所有的时间延时,都是支持的。可以在每个时间节点增加一个 round 字段,记录时间轮转动的圈数,比如对于延时 130s 的任务,round 就是 2,放在第 10 个时间刻度的链表中。这样当时间轮转到一个节点,执行节点上的任务时,首先判断 round 是否等于 0,如果等于 0,则把这个任务从任务链表中移出交给异步线程执行,否则将 round 减 1 继续检查后面的任务。

Untitled转存失败,建议直接上传图片文件

事务消息

事务消息并不是为了解决分布式事务,而是提供消息发送与业务落库的一致性,其实现原理就是一分布式事务的具体运用,请看如下示例:

https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/RocketMQ%20%e5%ae%9e%e6%88%98%e4%b8%8e%e8%bf%9b%e9%98%b6%ef%bc%88%e5%ae%8c%ef%bc%89/assets/20200726212339249.png

上述伪代码中,将订单存储关系型数据库中和将消息发送到 MQ 这是两个不同介质的两个操作,如果能保证消息发送、数据库存储这两个操作要么同时成功,要么同时失败,RocketMQ 为了解决该问题引入了事务消息

定时消息

开源版本的 RocketMQ 目前并不支持任意精度的定时消息。所谓的定时消息就是将消息发送到 Broker,但消费端不会立即消费,而是要f到指定延迟时间后才能被消费端消费。

RocketMQ(5.0之前) 目前支持指定级别的延迟,其延迟级别如下:

1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

5.0之后,支持秒级别的的延迟消息。

  • 实现原理t ,RocketMQ 定时消息引入了秒级的时间轮算法。时间轮是一种定时调度的数据结构,可以高效地处理定时任务。RocketMQ 的时间轮由多个时间槽组成,每个槽表示一个时间单位。槽内维护了一个链表,用于存储到达该时间槽的消息。个槽代表一个时间单位(例如,1秒)。时间槽的数量决定了时间轮的精度,即定时调度的最小时间间隔。当有延迟消息需要添加到时间轮中时,RocketMQ 计算出该消息距离触发的时间,将消息放入对应的时间槽中。RocketMQ 的时间轮以固定的速度滚动,每次滚动一个时间槽的间隔。当时间轮滚动时,消息将被移动到下一个槽中。这样,随着时间的推移,到达触发时间的消息将逐渐移动到时间轮的最前方。

消息过滤

消息过滤是指消费端可以根据某些条件对一个 Topic 中的消息进行过滤,即只消费一个主题下满足过滤条件的消息。

RocketMQ 目前主要的过滤机制是基于 Tag 的过滤与基于消息属性的过滤,基于消息属性的过滤支持 SQL92 表达式,对消息进行过滤。

消息发送核心

消息发送高可用设计与故障y规避机制

熟悉 RocketMQ 的小伙伴应该都知道,RocketMQ Topic 路由注册中心 NameServer 采用的是最终一致性模型,而且客户端是定时向 NameServtyer 更新 Topic 的路由信息,即客户端(Producer、Consumer)是无法实时感知 Broker 宕机的,这样消息发送者会继续向已宕机的 Broker 发送消息,造成消息发送异常。那 RocketMQ 是如何保证消息发送的高可用性呢?

RocketMQ 为了保证消息发送的高可用性,在内部引入了重试机制,默认重试 2 次。RocketMQ 消息发送端采取的队列负载均衡默认采用轮循。

在 RocketMQ 中消息发送者是线程安全的,即一个消息发送者可以在多线程环境中安全使用。每一个消息发送者全局会维护一个 Topic 上一次选择的队列,然后基于这个序号进行递增轮循,引入了 ThreadLocal 机制,即每一个发送者线程持有一个上一次选择的队列,用 sendWhichQueue 表示。

RocketMq面试题

消息队列有两种模型:队列模型发布/订阅模型

4.消息队列有哪些消息模型?

  • 队列模型

这是最初的一种消息队列模型,对应着消息队列“发-存-收”的模型。生产者往某个队列里面发送消息,一个队列可以存储多个生产者的消息,一个队列也可以有多个消费者,但是消费者之间是竞争关系,也就是说每条消息只能被一个消费者消费。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/84c458f4e543417db19bba70232dbde3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=AdscY%2FMg%2FXBJWAjKNlBKlNaV%2FdI%3D

队列模型

  • 发布/订阅模型

如果需要将一份消息数据分发给多个消费者,并且每个消费者都要求收到全量的消息。很显然,队列模型无法满足这个需求。解决的方式就是发布/订阅模型。

在发布 - 订阅模型中,消息的发送方称为发布者(Publisher),消息的接收方称为订阅者(Subscriber),服务端存放消息的容器称为主题(Topic)。发布者将消息发送到主题中,订阅者在接收消息之前需要先“订阅主题”。“订阅”在这里既是一个动作,同时还可以认为是主题在消费时的一个逻辑副本,每份订阅中,订阅者都可以接收到主题的所有消息。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/65a9fefca4204b65a575694b7e4dcb25~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=QBmsgaxScdldgRBqffWqFrM%2BjKg%3D

发布-订阅模型

它和 “队列模式” 的异同:生产者就是发布者,队列就是主题,消费者就是订阅者,无本质区别。唯一的不同点在于:一份消息数据是否可以被多次消费。

.那RocketMQ的消息模型呢?

RocketMQ使用的消息模型是标准的发布-订阅模型,在RocketMQ的术语表中,生产者、消费者和主题,与发布-订阅模型中的概念是完全一样的。

RocketMQ本身的消息是由下面几部分组成:

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/ed9aa6dcdd884155874652005ddb3605~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=Us8xbDfJObGY%2FKVLFZ0Pd3uPnKk%3D

RocketMQ消息的组成

  • Message

Message(消息)就是要传输的信息。

一条消息必须有一个主题(Topic),主题可以看做是你的信件要邮寄的地址。

一条消息也可以拥有一个可选的标签(Tag)和额处的键值对,它们可以用于设置一个业务 Key 并在 Broker 上查找此消息以便在开发期间查找问题。

  • Topic

Topic(主题)可以看做消息的归类,它是消息的第一级类型。比如一个电商系统可以分为:交易消息、物流消息等,一条消息必须有一个 Topic 。

Topic 与生产者和消费者的关系非常松散,一个 Topic 可以有0个、1个、多个生产者向其发送消息,一个生产者也可以同时向不同的 Topic 发送消息。

一个 Topic 也可以被 0个、1个、多个消费者订阅。

  • Tag

Tag(标签)可以看作子主题,它是消息的第二级类型,用于为用户提供额外的灵活性。使用标签,同一业务模块不同目的的消息就可以用相同 Topic 而不同的 Tag 来标识。比如交易消息又可以分为:交易创建消息、交易完成消息等,一条消息可以没有 Tag 。

标签有助于保持你的代码干净和连贯,并且还可以为 RocketMQ 提供的查询系统提供帮助。

  • Group

RocketMQ中,订阅者的概念是通过消费组(Consumer Group)来体现的。每个消费组都消费主题中一份完整的消息,不同消费组之间消费进度彼此不受影响,也就是说,一条消息被Consumer Group1消费过,也会再给Consumer Group2消费。

消费组中包含多个消费者,同一个组内的消费者是竞争消费的关系,每个消费者负责消费组内的一部分消息。默认情况,如果一条消息被消费者Consumer1消费了,那同组的其他消费者就不会再收到这条消息。

  • Message Queue

Message Queue(消息队列),一个 Topic 下可以设置多个消息队列,Topic 包括多个 Message Queue ,如果一个 Consumer 需要获取 Topic下所有的消息,就要遍历所有的 Message Queue。

RocketMQ还有一些其它的Queue——例如ConsumerQueue。

  • Offset

在Topic的消费过程中,由于消息需要被不同的组进行多次消费,所以消费完的消息并不会立即被删除,这就需要RocketMQ为每个消费组在每个队列上维护一个消费位置(Consumer Offset),这个位置之前的消息都被消费过,之后的消息都没有被消费过,每成功消费一条消息,消费位置就加一。

也可以这么说,Queue 是一个长度无限的数组,Offset 就是下标。

RocketMQ的消息模型中,这些就是比较关键的概念了。画张图总结一下:

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/95f47e031ed84b67a8def01349324997~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=C9rOGMRKJw17Co7Pln4Tbe2Hehg%3D

**#6.消息的消费模式了解吗?**

消息消费模式有两种:Clustering(集群消费)和Broadcasting(广播消费)。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/b5aadd3114b64577983706dfb3fe35dd~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=uQn3irWdxvJ5JnUU18UHn2w48Ns%3D

两种消费模式

默认情况下就是集群消费,这种模式下一个消费者组共同消费一个主题的多个队列,一个队列只会被一个消费者消费,如果某个消费者挂掉,分组内其它消费者会接替挂掉的消费者继续消费。

而广播消费消息会发给消费者组中的每一个消费者进行消费。

**#7.RoctetMQ基本架构了解吗?**

先看图,RocketMQ的基本架构:

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/7d21b8653ac44de5978147c173dcc561~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=4Jck24VqjSO2Od39hBNKOGlfAyE%3D

RocketMQ架构

RocketMQ 一共有四个部分组成:NameServer,Broker,Producer 生产者,Consumer 消费者,它们对应了:发现、发、存、收,为了保证高可用,一般每一部分都是集群部署的。

**#8.那能介绍一下这四部分吗?**

nameServer

NameServer 是一个无状态的服务器,角色类似于 Kafka使用的 Zookeeper,但比 Zookeeper 更轻量。

特点:

  • 每个 NameServer 结点之间是相互独立,彼此没有任何信息交互。
  • Nameserver 被设计成几乎是无状态的,通过部署多个结点来标识自己是一个伪集群,Producer 在发送消息前从 NameServer 中获取 Topic 的路由信息也就是发往哪个 Broker,Consumer 也会定时从 NameServer 获取 Topic 的路由信息,Broker 在启动时会向 NameServer 注册,并定时进行心跳连接,且定时同步维护的 Topic 到 NameServer。

功能主要有两个:

  • 1、和Broker 结点保持长连接。
  • 2、维护 Topic 的路由信息。

Broker

消息存储和中转角色,负责存储和转发消息。

  • Broker 内部维护着一个个 Consumer Queue,用来存储消息的索引,真正存储消息的地方是 CommitLog(日志文件)。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/cf328784b9204b259b03f8b56110d83f~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=4W%2BIhd1td5EqfnnWfNYugeGsItA%3D

CommitLog

是消息主体以及元数据存储主体,每个节点只有一个,客户端写入到所有 MessageQueue 的数据,最终都会存储到这一个文件中。

ConsumeQueue

是逻辑消费队列,是消息消费的索引,不存储具体的消息数据。引入的 目的主要是提高消息消费的性能。由于 RocketMQ 是基于主题 Topic 的订阅模式,消息消 费是针对主题进行的,如果要遍历 Commitlog 文件,基于 Topic 检索消息是非常低效 的。Consumer 可根据 ConsumeQueue 来查找待消费的消息,ConsumeQueue 文件可 以看成是基于 Topic 的 CommitLog 索引文件。 IndexFile 是索引文件,它在文件系统中是以 HashMap 结构存储的。在 RocketMQ 中, 通过 Key 或时间区间来查询消息的功能就是由它实现的。

因为消息数据会很多,CommitLog 会存储所有的消息内容。所以为了保证数 据的读写性能,我们会对 CommitLog 进行分段存储。删除按照节点维度

RocketMQ存储-图片来源官网

  • 单个 Broker 与所有的 Nameserver 保持着长连接和心跳,并会定时将 Topic 信息同步到 NameServer,和 NameServer 的通信底层是通过 Netty 实现的。

**#Producer**

  • Producer由用户进行分布式部署,消息由Producer通过多种负载均衡模式发送到Broker集群,发送低延时,支持快速失败。

消息生产者,业务端负责发送消息,由用户自行实现和分布式部署。

  • RocketMQ 提供了三种方式发送消息:同步、异步和单向
  • 同步发送:同步发送指消息发送方发出数据后会在收到接收方发回响应之后才发下一个数据包。一般用于重要通知消息,例如重要通知邮件、营销短信。
  • 异步发送:异步发送指发送方发出数据后,不等接收方发回响应,接着发送下个数据包,一般用于可能链路耗时较长而对响应时间敏感的业务场景,例如用户视频上传后通知启动转码服务。
  • 单向发送:单向发送是指只负责发送消息而不等待服务器回应且没有回调函数触发,适用于某些耗时非常短但对可靠性要求并不高的场景,例如日志收集。

**#Consumer**

消息消费者,负责消费消息,一般是后台系统负责异步消费。

  • Consumer也由用户部署,支持PUSH和PULL两种消费模式,支持集群消费广播消费,提供实时的消息订阅机制
  • Pull:拉取型消费者(Pull Consumer)主动从消息服务器拉取信息,只要批量拉取到消息,用户应用就会启动消费过程,所以 Pull 称为主动消费型。
  • Push:推送型消费者(Push Consumer)封装了消息的拉取、消费进度和其他的内部维护工作,将消息到达时执行的回调接口留给用户应用程序来实现。所以 Push 称为被动消费类型,但其实从实现上看还是从消息服务器中拉取消息,不同于 Pull 的是 Push 首先要注册消费监听器,当监听器处触发后才开始消费消息。

.如何保证消息的可用性/可靠性/不丢失呢?

消息可能在哪些阶段丢失呢?可能会在这三个阶段发生丢失:生产阶段、存储阶段、消费阶段。

所以要从这三个阶段考虑:

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/9160a649fc0240d085adb060f211d94a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=NnfQU1rAV%2BPnV6aVFqrsQ%2BhUdPE%3D

消息传递三阶段

**#生产**

在生产阶段,主要通过请求确认机制,来保证消息的可靠传递

  • 1、同步发送的时候,要注意处理响应结果和异常。如果返回响应OK,表示消息成功发送到了Broker,如果响应失败,或者发生其它异常,都应该重试。
  • 2、异步发送的时候,应该在回调方法里检查,如果发送失败或者异常,都应该进行重试。
  • 3、如果发生超时的情况,也可以通过查询日志的API,来检查是否在Broker存储成功。

**#存储**

存储阶段,可以通过配置可靠性优先的 Broker 参数来避免因为宕机丢消息,简单说6就是可靠性优先的场景都应该使用同步。

  • 1、消息只要持久化到CommitLog(日志文件)中,即使Broker宕机,未消费的消息也能重新恢复再消费。
  • 2、Broker的刷盘机制:同步刷盘和异步刷盘,不管哪种刷盘都可以保证消息一定存储在pagecache中(内存中),但是同步刷盘更可靠,它是Producer发送消息后等数据持久化到磁盘之后再返回响应给Producer。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/350ca36dfaeb4db3b677c74dbdd2afab~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=PUbn6LRopCd1rUb%2Bb5moET%2FLmjs%3D

同步刷盘和异步刷盘-图片来源官网

  • 3、Broker通过主从模式来保证高可用,Broker支持Master和Slave同步复制、Master和Slave异步复制模式,生产者的消息都是发送给Master,但是消费既可以从Master消费,也可以从Slave消费。同步复制模式可以保证即使Master宕机,消息肯定在Slave中有备份,保证了消息不会丢失。

0.如何处理消息重复的问题呢?

对分布式消息队列来说,同时做到确保一定投递和不重复投递是很难的,就是所谓的“有且仅有一次” 。RocketMQ择了确保一定投递,保证消息不丢失,但有可能造成消息重复。

处理消息重复问题,主要有业务端自己保证,主要的方式有两种:业务幂等消息去重

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/0fc7cb7e61194eaa8686ac0cb27e4768~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=npnTLXHGkDbAq7uUOJ8LyhV8zao%3D

消息重复处理

业务幂等:第一种是保证消费逻辑的幂等性,也就是多次调用和一次调用的效果是一样的。这样一来,不管消息消费多少次,对业务都没有影响。

消息去重:第二种是业务端,对重复的消息就不再消费了。这种方法,需要保证每条消息都有一个惟一的编号,通常是业务相关的,比如订单号,消费的记录需要落库,而且需要保证和消息确认这一步的原子性。

具体做法是可以建立一个消费记录表,拿到这个消息做数据库的insert操作。给这个消息做一个唯一主键(primary key)或者唯一约束,那么就算出现重复消费的情况,就会导致主键冲突,那么就不再处理这条消息。

**#11.怎么处理消息积压?

发生了消息积压,这时候就得想办法赶紧把积压的消息消费完,就得考虑提高消费能力,一般有两种办法:

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/9a871065c7c4484ea0311fe9cec32b32~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=7NzTMOnPfWL%2Bd8eI2t0yggvtSwg%3D

消息积压处理

  • 消费者扩容:如果当前Topic的Message Queue的数量大于消费者数量,就可以对消费者进行扩容,增加消费者,来提高消费能力,尽快把积压的消息消费玩。
  • 消息迁移Queue扩容:如果当前Topic的Message Queue的数量小于或者等于消费者数量,这种情况,再扩容消费者就没什么用,就得考虑扩容Message Queue。可以新建一个临时的Topic,临时的Topic多设置一些Message Queue,然后先用一些消费者把消费的数据丢到临时的Topic,因为不用业务处理,只是转发一下消息,还是很快的。接下来用扩容的消费者去消费新的Topic里的数据,消费完了之后,恢复原状。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/20703db9aca04d3392aad18c6d654380~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=czn0TuahiVK9%2Bpqql8y%2BkOCXZcg%3D

消息迁移扩容消费

**#12.顺序消息如何实现?**

顺序消息是指消息的消费顺序和产生顺序相同,在有些业务逻辑下,必须保证顺序,比如订单的生成、付款、发货,这个消息必须按顺序处理才行。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/7d8b3d8c4f01472fa0391507e9a73375~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=%2BJkxBjP8oyz1OW1ksdwvzcSISvo%3D

顺序消息

顺序消息分为全局顺序消息和部分顺序消息,全局顺序消息指某个 Topic 下的所有消息都要保证顺序;

部分顺序消息只要保证每一组消息被顺序消费即可,比如订单消息,只要保证同一个订单 ID 个消息能按顺序消费即可。

**#部分顺序消息**

部分顺序消息相对比较好实现,生产端需要做到把同 ID 的消息发送到同一个 Message Queue ;在消费过程中,要做到从同一个Message Queue读取的消息顺序处理——消费端不能并发处理顺序消息,这样才能达到部分有序。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/bd7ccc2c3e4b47d384710da8eb7b1040~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=%2FaenZ%2B1%2BUJu%2BntgOeZ4adp5ZVjQ%3D

部分顺序消息

发送端使用 MessageQueueSelector 类来控制 把消息发往哪个 Message Queue 。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/0776dedb3bc54075b1686f876d1e08b3~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=FFx5QZDUWtlEuiMeZGjoVsFP6IY%3D

顺序消息生产-例子来源官方

消费端通过使用 MessageListenerOrderly 来解决单 Message Queue 的消息被并发处理的问题。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/8d79c6cab6fb49a282f001e5d48136e4~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=4091V9bd8%2Fpf5V6o2h5hLl68d3Q%3D

**#全局顺序消息**

RocketMQ 默认情况下不保证顺序,比如创建一个 Topic ,默认八个写队列,八个读队列,这时候一条消息可能被写入任意一个队列里;在数据的读取过程中,可能有多个 Consumer ,每个 Consumer 也可能启动多个线程并行处理,所以消息被哪个 Consumer 消费,被消费的顺序和写人的顺序是否一致是不确定的。

要保证全局顺序消息, 需要先把 Topic 的读写队列数设置为 一,然后Producer Consumer 的并发设置,也要是一。简单来说,为了保证整个 Topic全局消息有序,只能消除所有的并发处理,各部分都设置成单线程处理 ,这时候就完全牺牲RocketMQ的高并发、高吞吐的特性了。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/c43532c3e5f441c183f43e9930d03d94~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=ifzf6R2igNwB5zuPXKhmuNlgY5g%3D

全局顺序消息

**#13.如何实现消息过滤?**

有两种方案:

  • 一种是在 Broker 端按照 Consumer 的去重逻辑进行过滤,这样做的好处是避免了无用的消息传输到 Consumer 端,缺点是加重了 Broker 的负担,实现起来相对复杂。
  • 另一种是在 Consumer 端过滤,比如按照消息设置的 tag 去重,这样的好处是实现起来简单,缺点是有大量无用的消息到达了 Consumer 端只能丢弃不处理。

一般采用Cosumer端过滤,如果希望提高吞吐量,可以采用Broker过滤。

对消息的过滤有三种方式:

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/ebbe670458a64756945cf7fa9f3fae75~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=FzfQ%2BGog0Sc%2FTDjmQ6BFoN1WTQ4%3D

消息过滤

  • 根据Tag过滤:这是最常见的一种,用起来高效简单
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_EXAMPLE");
consumer.subscribe("TOPIC", "TAGA || TAGB || TAGC");
  • SQL 表达式过滤:SQL表达式过滤更加灵活
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_4");
// 只有订阅的消息有这个属性a, a >=0 and a <= 3
consumer.subscribe("TopicTest", MessageSelector.bySql("a betw#een 0 and 3");
consumer.registerMessageListener(new MessageListenerConcurrently() {
   @Override
   public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
       return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
   }
});
consumer.start();
​
  • Filter Server 方式:最灵活,也是最复杂的一种方式,允许用户自定义函数进行过滤

**#14.延迟消息了解吗?**

电商的订单超时自动取消,就是一个典型的利用延时消息的例子,用户提交了一个订单,就可以发送一个延时消息,1h后去检查这个订单的状态,如果还是未付款就取消订单释放库存。

RocketMQ是支持延时消息的,只需要在生产消息的时候设置消息的延时级别:

// 实例化一个生产者来产生延时消息
DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup");
// 启动生产者
producer.start();
int totalMessagesToSend = 100;
for (int i = 0; i < totalMessagesToSend; i++) {
    Message message = new Message("TestTopic", ("Hello scheduled message " + i).getBytes());
    // 设置延时等级3,这个消息将在10s之后发送(现在只支持固定的几个时间,详看delayTimeLevel)
    message.setDelayTimeLevel(3);
    // 发送消息
    producer.send(message);
}
​

但是目前RocketMQ支持的延时级别是有限的:

private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";

**#RocketMQ怎么实现延时消息的?**

简单,八个字:临时存储+定时任务

Broker收到延时消息了,会先发送到主题(SCHEDULE_TOPIC_XXXX)的相应时间段的Message Queue中,然后通过一个定时任务轮询这些队列,到期后,把消息投递到目标Topic的队列中,然后消费者就可以正常消费这些消息。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/9c6187a2573c49f49338e3b8cf1560ea~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=t66rI8YmPYRw6Td62Ud3wimYi5g%3D

延迟消息处理流程-图片来源见水印

**#15.怎么实现分布式消息事务的?半消息?**

半消息:是指暂时还不能被 Consumer 消费的消息,Producer 成功发送到 Broker 端的消息,但是此消息被标记为 “暂不可投递” 状态,只有等 Producer 端执行完本地事务后经过二次确认了之后,Consumer 才能消费此条消息。

依赖半消息,可以实现分布式消息事务,其中的关键在于二次确认以及消息回查:

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/3a858ecad46340b488664f6899ed24d7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=0jQMxQC9mz45jEwHAfNLeung8Wo%3D

RocketMQ实现消息事务

  • 1、Producer 向 broker 发送半消息
  • 2、Producer 端收到响应,消息发送成功,此时消息是半消息,标记为 “不可投递” 状态,Consumer 消费不了。
  • 3、Producer 端执行本地事务。
  • 4、正常情况本地事务执行完成,Producer 向 Broker 发送 Commit/Rollback,如果是 Commit,Broker 端将半消息标记为正常消息,Consumer 可以消费,如果是 Rollback,Broker 丢弃此消息。
  • 5、异常情况,Broker 端迟迟等不到二次确认。在一定时间后,会查询所有的半消息,然后到 Producer 端查询半消息的执行情况。
  • 6、Producer 端查询本地事务的状态
  • 7、根据事务的状态提交 commit/rollback 到 broker 端。(5,6,7 是消息回查)
  • 8、消费者段消费到消息之后,执行本地事务,执行本地事务。

16.死信队列知道吗?

死信队列用于处理无法被正常消费的消息,即死信消息。

当一条消息初次消费失败,消息队列 RocketMQ 会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 RocketMQ 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中,该特殊队列称为死信队列

死信消息的特点

  • 不会再被消费者正常消费。
  • 有效期与正常消息相同,均为 3 天,3 天后会被自动删除。因此,需要在死信消息产生后的 3 天内及时处理。

死信队列的特点

  • 一个死信队列对应一个 Group ID, 而不是对应单个消费者实例。
  • 如果一个 Group ID 未产生死信消息,消息队列 RocketMQ 不会为其创建相应的死信队列。
  • 一个死信队列包含了对应 Group ID 产生的所有死信消息,不论该消息属于哪个 Topic。

RocketMQ 控制台提供对死信消息的查询、导出和重发的功能。

**#17.如何保证RocketMQ的高可用?**

NameServer因为是无状态,且不相互通信的,所以只要集群部署就可以保证高可用。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/a0a629fc45f74ff0aa8868b559c27c85~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=iFao3r7pKgOFt5IHEuzCh1LWrkE%3D

NameServer集群

RocketMQ的高可用主要是在体现在Broker的读和写的高可用,Broker的高可用是通过集群主从实现的。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/d3656b9288ef4b059205fc82dcd9318e~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=72%2F4du5z4PhE8FL1MNFM%2FJv10kI%3D

Broker集群、主从示意图

Broker可以配置两种角色:Master和Slave,Master角色的Broker支持读和写,Slave角色的Broker只支持读,Master会向Slave同步消息。

也就是说Producer只能向Master角色的Broker写入消息,Cosumer可以从Master和Slave角色的Broker读取消息。

Consumer 的配置文件中,并不需要设置是从 Master 读还是从 Slave读,当 Master 不可用或者繁忙的时候, Consumer 的读请求会被自动切换到从 Slave。有了自动切换 Consumer 这种机制,当一个 Master 角色的机器出现故障后,Consumer 仍然可以从 Slave 读取消息,不影响 Consumer 读取消息,这就实现了读的高可用。

如何达到发送端写的高可用性呢?在创建 Topic 的时候,把 Topic 的多个Message Queue 创建在多个 Broker 组上(相同 Broker 名称,不同 brokerId机器组成 Broker 组),这样当 Broker 组的 Master 不可用后,其他组Master 仍然可用, Producer 仍然可以发送消息 RocketMQ 目前还不支持把Slave自动转成 Master ,如果机器资源不足,需要把 Slave 转成 Master ,则要手动停止 Slave 色的 Broker ,更改配置文件,用新的配置文件启动 Broker。

GitHub 上标星 10000+ 的开源知识库《二哥的 Java 进阶之路open in new window》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,500+张手绘图,可以说是通俗易懂、风趣幽默……详情戳:太赞了,GitHub 上标星 10000+ 的 Java 教程open in new window

微信搜 沉默王二 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 222 即可免费领取。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/9dbedf7b7cba4062b5461d6d14f6809a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=PFgbZ2r%2FK8MSCT7lnD%2FvUXvkueY%3D

**#原理**

**#18.说一下RocketMQ的整体工作流程?**

简单来说,RocketMQ是一个分布式消息队列,也就是消息队列+分布式系统

作为消息队列,它是--的一个模型,对应的就是Producer、Broker、Cosumer;作为分布式系统,它要有服务端、客户端、注册中心,对应的就是Broker、Producer/Consumer、NameServer

所以我们看一下它主要的工作流程:RocketMQ由NameServer注册中心集群、Producer生产者集群、Consumer消费者集群和若干Broker(RocketMQ进程)组成:

  1. Broker在启动的时候去向所有的NameServer注册,并保持长连接,每30s发送一次心跳
  2. Producer在发送消息的时候从NameServer获取Broker服务器地址,根据负载均衡算法选择一台服务器来发送消息
  3. Conusmer消费消息的时候同样从NameServer获取Broker地址,然后主动拉取消息来消费

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/1b8afa16933e48169a001f87a117577b~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=NBCkLodIYryhBWRirNC1%2FNmuBHo%3D

RocketMQ整体工作流程

**#19.为什么RocketMQ不使用Zookeeper作为注册中心呢?**

Kafka我们都知道采用Zookeeper作为注册中心——当然也开始逐渐去Zookeeper,RocketMQ不使用Zookeeper其实主要可能从这几方面来考虑:

  1. 基于可用性的考虑,根据CAP理论,同时最多只能满足两个点,而Zookeeper满足的是CP,也就是说Zookeeper并不能保证服务的可用性,Zookeeper在进行选举的时候,整个选举的时间太长,期间整个集群都处于不可用的状态,而这对于一个注册中心来说肯定是不能接受的,作为服务发现来说就应该是为可用性而设计。
  2. 基于性能的考虑,NameServer本身的实现非常轻量,而且可以通过增加机器的方式水平扩展,增加集群的抗压能力,而Zookeeper的写是不可扩展的,Zookeeper要解决这个问题只能通过划分领域,划分多个Zookeeper集群来解决,首先操作起来太复杂,其次这样还是又违反了CAP中的A的设计,导致服务之间是不连通的。
  3. 持久化的机制来带的问题,ZooKeeper 的 ZAB 协议对每一个写请求,会在每个 ZooKeeper 节点上保持写一个事务日志,同时再加上定期的将内存数据镜像(Snapshot)到磁盘来保证数据的一致性和持久性,而对于一个简单的服务发现的场景来说,这其实没有太大的必要,这个实现方案太重了。而且本身存储的数据应该是高度定制化的。
  4. 消息发送应该弱依赖注册中心,而RocketMQ的设计理念也正是基于此,生产者在第一次发送消息的时候从NameServer获取到Broker地址后缓存到本地,如果NameServer整个集群不可用,短时间内对于生产者和消费者并不会产生太大影响。

**#20.Broker是怎么保存数据的呢?**

RocketMQ主要的存储文件包括CommitLog文件、ConsumeQueue文件、Indexfile文件。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/f64081c4c5fe41e0aabeb845386497f5~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=vz4iYYBQxS747tEatp1jsOblqNg%3D

消息存储的整体的设计:

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/66161cd3b463488399e4d4f0f84b277a~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=pPpPU1z3OfiHnJ511n%2BlXTDDumg%3D

消息存储整体设计-来源官网

  • CommitLog:消息主体以及元数据的存储主体,存储Producer端写入的消息主体内容,消息内容不是定长的。单个文件大小默认1G, 文件名长度为20位,左边补零,剩余为起始偏移量,比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。消息主要是顺序写入日志文件,当文件满了,写入下一个文件。

CommitLog文件保存于${Rocket_Home}/store/commitlog目录中,从图中我们可以明显看出来文件名的偏移量,每个文件默认1G,写满后自动生成一个新的文件。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/81cf7bbaf2a74977adede35277fd94bf~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=AGKZEEJaYWRT95DW5GDf3EZFQ%2BA%3D

CommitLog

  • ConsumeQueue:消息消费队列,引入的目的主要是提高消息消费的性能,由于RocketMQ是基于主题topic的订阅模式,消息消费是针对主题进行的,如果要遍历commitlog文件中根据topic检索消息是非常低效的。

Consumer即可根据ConsumeQueue来查找待消费的消息。其中,ConsumeQueue(逻辑消费队列)作为消费消息的索引,保存了指定Topic下的队列消息在CommitLog中的起始物理偏移量offset,消息大小size和消息Tag的HashCode值。

ConsumeQueue文件可以看成是基于Topic的CommitLog索引文件,故ConsumeQueue文件夹的组织方式如下:topic/queue/file三层组织结构,具体存储路径为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。同样ConsumeQueue文件采取定长设计,每一个条目共20个字节,分别为8字节的CommitLog物理偏移量、4字节的消息长度、8字节tag hashcode,单个文件由30W个条目组成,可以像数组一样随机访问每一个条目,每个ConsumeQueue文件大小约5.72M;

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/42e0be45b2e5496bb75e03988dbc0d45~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=AKj6tPu%2Fjc2BUaqRVLIed361Jl8%3D

Comsumer Queue

  • IndexFile:IndexFile(索引文件)提供了一种可以通过key或时间区间来查询消息的方法。Index文件的存储位置是: {fileName},文件名fileName是以创建时的时间戳命名的,固定的单个IndexFile文件大小约为400M,一个IndexFile可以保存 2000W个索引,IndexFile的底层存储设计为在文件系统中实现HashMap结构,故RocketMQ的索引文件其底层实现为hash索引。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/b7b64127eb1b4116b829493f9fe6cdd7~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=DNAJzQrjmcDFrwbWXWhg%2BCBN%2Be8%3D

IndexFile文件示意图

总结一下:RocketMQ采用的是混合型的存储结构,即为Broker单个实例下所有的队列共用一个日志数据文件(即为CommitLog)来存储。

RocketMQ的混合型存储结构(多个Topic的消息实体内容都存储于一个CommitLog中)针对Producer和Consumer分别采用了数据和索引部分相分离的存储结构,Producer发送消息至Broker端,然后Broker端使用同步或者异步的方式对消息刷盘持久化,保存至CommitLog中。

只要消息被刷盘持久化至磁盘文件CommitLog中,那么Producer发送的消息就不会丢失。正因为如此,Consumer也就肯定有机会去消费这条消息。当无法拉取到消息后,可以等下一次消息拉取,同时服务端也支持长轮询模式,如果一个消息拉取请求未拉取到消息,Broker允许等待30s的时间,只要这段时间内有新消息到达,将直接返回给消费端。

这里,RocketMQ的具体做法是,使用Broker端的后台服务线程—ReputMessageService不停地分发请求并异步构建ConsumeQueue(逻辑消费队列)和IndexFile(索引文件)数据。

https://p0-xtjj-private.juejin.cn/tos-cn-i-73owjymdk6/e2e22e1f00f4434aa047b7a67b531634~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAgRWxlZQ==:q75.awebp?policy=eyJ2bSI6MywidWlkIjoiOTU4NDI5ODcyNzg2NTEwIn0%3D&rk3s=e9ecf3d6&x-orig-authkey=f32326d3454f2ac7e96d3d06cdbb035152127018&x-orig-expires=1734949884&x-orig-sign=BG0GKWmE6ceJtguqDgOQmjNEFVI%3D

/

  1. 协议层支持 Remoting 和 gRPC 两种协议。 网络层是基于 Java NIO 框架 Netty 开发,底层也是通过多路复用、异步 IO、Reactor 模 型等技术来提高网络模块的性能。

存储层是基于多个 MessageQueue 的数据统一存储到一个文件的思路来设计的,同时也 支持分段存储和基于时间的数据过期机制。 3.

  1. 元数据存储是使用 Broker + 自定义的 NameServer 之间的配合来实现的。
  2. 客户端的访问需要经过客户端寻址机制,拿到元数据信息后,才直连 Broker。 生产端是将数据写入到 Topic 或分区,写入 Topic 时需要经过生产分区分配操作,确认最 终写入的 MessageQueue 也支持多种写入方式。 6. 消费端有消费分组的概念,也需要在多个消费者和消费分组之间进行消费的负载均衡,最后 通过提交消费位点的形式来保存消费进度。

参考引用