JavaEE 企业级分布式高级架构师(二十)RocketMQ学习笔记(6)

本文围绕RocketMQ展开源码解析。先介绍环境搭建,接着深入分析NameServer、Broker、Producer、Consumer的启动流程、核心机制等,如NameServer的心跳机制、Broker的路由注册。还详细阐述消息存储,包括CommitLog、ConsumeQueue、Index等文件的结构、特点及刷盘、清除机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

源码篇

环境搭建

源码拉取

在这里插入图片描述

调试

  • 创建conf配置文件从distribution拷⻉broker.conf、logback_broker.xml和logback_namesrv.xml。
  • 配置启动的工作目录、环境变量已经参数:

在这里插入图片描述

在这里插入图片描述

  • 启动Namesrv

在这里插入图片描述

  • 启动Broker

在这里插入图片描述

  • 发送消息

在这里插入图片描述

  • 消费消息

在这里插入图片描述

NameServer

  • 整个namesrv工程代码量很少,结构也很简单:

在这里插入图片描述

启动流程

  • 启动NameServer的第一步是构造一个NamesrvController实例,这个类是NameServer的核心类。然后初始化、启动 NamesrvController 类,而 createNamesrvController 方法,就是从命令行接收参数,然后将解析成配置类 NamesrvConfig 和 NettyServerConfig 。

在这里插入图片描述

  • 我们知道在命令行中运行 RocketMQ 是可以指定参数的,它的原理就是下面代码展示的那样。
  • 通过 -c 命令可以指定配置文件,将配置文件中的内容解析成 java.util.Properties 类,然后赋值给 NamesrvConfig 和 NettyServerConfig 类完成配置文件的解析与映射。
  • 如果指定了 -p 命令,则会在控制台打印配置信息,然后程序直接退出。
  • 除此之外还可以使用 -n 参数指定 namesrvAddr 的值,这是在 org.apache.rocketmq.srvutil.ServerUtil # buildCommandlineOptions 方法中指定的参数。
  • 当完成配置属性的映射,就会根据配置类 NamesrvConfig 和 NettyServerConfig 构造一 个 NamesrvController 实例。

在这里插入图片描述

初始化及心跳机制

  • 启动 NameServer 的第二步是通过 NamesrvController#initialize 完成初始化。

在这里插入图片描述

  • 看一看initialize方法:

在这里插入图片描述

  • 在初始化 NamesrvController 过程中,会注册一个心跳机制的线程池,它会在启动后5秒开始每隔 10 秒扫描一次不活跃的 broker 。

在这里插入图片描述

  • 可以看到,在 scanNotActiveBroker 方法中, NameServer 会遍历 RouteInfoManager#brokerLiveTable 这个属性。

在这里插入图片描述

  • RouteInfoManager#brokerLiveTable 属性存储的是集群中所有 broker 的活跃信息,主要是 BrokerLiveInfo # lastUpdateTimestamp 属性,它描述了 broker 上一次更新的活跃时间戳。若lastUpdateTimestamp 属性超过120秒未更新,则该 broker 会被视为失效并从 brokerLiveTable 中移除。

优雅停机

  • NameServer 启动的最后一步,是注册了一个 JVM 的钩子函数,它会在 JVM 关闭之前执行。这个钩子函数的作用是释放资源,如关闭 Netty 服务器,关闭线程池等。

在这里插入图片描述

小结一下

NamesrvStartup#main:主函数启动
	|--NamesrvStartup#createNamesrvController:构造一个NamesrvController实例
		|--new NamesrvController:根据配置类 NamesrvConfig 和 NettyServerConfig 构造一个 NamesrvController 实例
	|--NamesrvStartup#start:初始化、启动 NamesrvController 类
		|--NamesrvController#initialize:初始化,加载配置,注册线程池
			|--RouteInfoManager#scanNotActiveBroker:心跳线程池,它会在启动后5秒开始每隔10秒扫描一次不活跃的broker
		|--NamesrvController#start:启动
		|--Runtime.getRuntime().addShutdownHook(new ShutdownHookThread):优雅停机

Broker

  • 消息中转⻆色,负责存储消息、转发消息。Broker在RocketMQ系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备。代理服务器也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等。

Broker作用

作用

  • 消息存储
  • 消息投递
  • 消息查询
  • 服务的高可用

包含的模块

  • Remoting Module:整个Broker的实体,负载处理来自clients端的请求。
  • Client Manager:负载管理可客户端(Producer、Consumer)和维护Consumer的Topic订阅信息。
  • Store Service:提供方便简单的API接口处理消息存储到物理硬盘和查询功能。
  • HA Service:高可用服务,提供Master Broker和Slave Broker之间的数据同步功能。
  • Index Service:根据特定的Message Key对投递到Broker的消息进行索引服务,以提供消息的快速查询。

启动流程

  • Broker的启动流程与NameServer的启动流程很类似,大体分为两步:
  • BrokerController创建
    • NettyServer和NettyClient的配置处理
    • 命令行参数的处理
    • Broker⻆色的处理
    • 创建BrokerController
    • 初始化BrokerController通过调用方法
  • BrokerController启动

在这里插入图片描述

BrokerController创建
  • 配置Netty、创建三个配置对象、监听端口、填充配置文件:

在这里插入图片描述

  • 获取namesrv地址;设置brokerId:主要是处理在集群条件下的Broker⻆色的,从这里看出来brokerId为 MixAll.MASTER_ID = 0 的为Mater节点,其他的为Slave 节点。

在这里插入图片描述

  • 处理命令行参数,初始化创建并返回controller

在这里插入图片描述

BrokerController启动

在这里插入图片描述

  • 可以看到在启动broker时,也启动了一个线程池,每隔一段时间(默认30秒)向nameserver服务端注册信息。

路由注册

broker发送心跳到nameserver

在这里插入图片描述

  • BrokerOuterAPI#registerBrokerAll

在这里插入图片描述

  • 分别给每个nameserver注册broker

在这里插入图片描述

nameserver处理请求包

在这里插入图片描述

  • 处理broker心跳注册broker

在这里插入图片描述

  • 路由元数据信息

在这里插入图片描述

路由删除

在这里插入图片描述

  • RocktMQ 有两个触发点来触发路由删除:
    • NameServer定时扫描 brokerLiveTable检测上次心跳包与 当前系统时间的时间差, 如果时间戳大于 120s,则需要移除该 Broker 信息。
    • Broker在正常被关闭的情况下,会执行 unrgisterBroker 指令。
  • 这两种方式删除路由的方法都是一样的,就是从路由表中删除与broker相关的信息。

在这里插入图片描述

  • 维护其他的路由信息表

在这里插入图片描述

在这里插入图片描述

路由发现

  • 路由发现是非实时的,当topic路由出现变化,NameServer不主动推送给客户端,而是由客户端定时拉去主题最新的路由。
  • 根据主题获取路由信息,NameServer不主动推,保证nameserver简单高效

在这里插入图片描述

  • 进行路由的发现

在这里插入图片描述

小结一下

BrokerStartup#main:主函数启动
	|--BrokerStartup#createBrokerController:创建一个BrokerController实例
		|--createBrokerController:配置Netty、创建三个配置对象、监听端口、处理命令行参数、初始化创建并返回controller
		|--Runtime.getRuntime().addShutdownHook(...):优雅停机
	|--BrokerStartup#start
		|--start:各种服务启动、创建定时线程向nameserver注册信息
			|--BrokerController#doRegisterBrokerAll:注册路由
				|--BrokerOuterAPI#registerBrokerAll:注册broker信息
					|--BrokerOuterAPI#registerBroker:分别给每个nameserver注册broker
						|--RemotingClient#invokeOneway:不接收返回值,异步的方式
						|--RemotingClient#invokeSync:同步方式,接收返回值
						
DefaultRequestProcessor#processRequest:nameserver处理broker发送的心跳请求包
	|--DefaultRequestProcessor#registerBroker:注册broker
		|--NamesrvController.getRouteInfoManager()#registerBroker:注册broker
	|--DefaultRequestProcessor#getRouteInfoByTopic:根据主题获取路由信息
		|--NamesrvController.getRouteInfoManager()#pickupTopicRouteData:根据nameserverControler拿到RouteInfoManager,根据路由获取相关的路由信息来填充TopicRouteData对象

在这里插入图片描述

Producer

  • RocketMQ发送消息分为三种实现方式:同步发送、异步发送、单向发送。目前的MQ中间件从存储模型来看,分为需要持久化和不需要持久化两种。

消费生产者组件

  • 生产者接口MQProducer

在这里插入图片描述

  • 接口实现类:

在这里插入图片描述

  • DefaultMQProducer:默认实现,没有实现发送事务消息的方法。
  • TransactionMQProducer:继承自DefaultMQProducer,实现了发送事务消息的方法。

RocketMQ消息

  • rocketmq 消息封装类org.apache.rocketmq.common.message.Message

在这里插入图片描述

  • 隐藏属性
    • tags:消息TAG,用于消息过滤。
    • keys:消息索引键。
    • waitStoreMsgOK:消息发送时是否等消息存储完成后再返回。
    • DELAY:消息延迟级别,用于定时消息或消息重试。
  • 对于一些关键属性,Message类提供了一组set接口来进行设置:

在这里插入图片描述

  • 这几个set接口对应的作用分别为为:
属性接口用途
MessageConst.PROPERTY_TAGSsetTags在消费消息时可以通过tag进行消息过滤判定
MessageConst.PROPERTY_KEYSsetKeys可以设置业务相关标识,用于消费处理判定,或消息追踪查询
MessageConst.PROPERTY_DELAY_TIME_LEVELsetDelayTimeLevel消息延迟处理级别,不同级别对应不同延迟时间
MessageConst.PROPERTY_WAIT_STORE_MSG_OKsetWaitStoreMsgOK在同步刷盘情况下是否需要等待数据落地才认为消息发送成功
MessageConst.PROPERTY_BUYER_IDsetBuyerId没有在代码中找到使用的地方,所以暂不明白其用处

这几个字段为什么用属性定义,而不是单独用一个字段进行表示?

  • 方便之处可能在于消息数据存盘结构早早定义,一些后期添加上的字段功能为了适应之前的存储结构,以属性形式存储在一个动态字段更为方便,自然兼容。
MessageExt
  • 对于发送方来说,上述Message的定义以足够。但对于RocketMQ的整个处理流程来说,还需要更多的字段信息用以记录一些必要内容,比如消息的id、创建时间、存储时间等等。

在这里插入图片描述

  • Message还有一个名为 MessageConst.PROPERTY_UNIQ_CLIENT_MESSAGE_ID_KEYIDX(UNIQ_KEY) 的属性, 在消息发送时由Producer生成创建。而 msgId 则是消息在Broker端进行存储时通过 MessageDecoder.createMessageId 方法生成的,其构成为:

在这里插入图片描述

  • 这个MsgId是在Broker生成的,Producer在发送消息时没有该信息,Consumer 在消费消息时则能获取到该值。
    在这里插入图片描述

生产者启动流程

  • 在创建好Producer之后,使用它来发消息之前,需要先启动它,即调用它的start()方法,代码如下:

在这里插入图片描述

  • 主要看DefaultMQProducerImpl的启动,代码如下:

在这里插入图片描述

消息发送流程

  • 消息发送流程主要是:验证消息、查找路由、消息发送(包含异常处理机制)
  • 消息验证,主要是进行消息的⻓度验证。
  • 一个topic有多个队列,分散在不同的broker。producer在发送消息的时候,需要选择一个队列。
  • producer发送消息全局时序图如下:

在这里插入图片描述

同步发送
  • DefaultMQProducer#send(Message)

在这里插入图片描述

  • DefaultMQProducerImpl#sendDefaultImpl()

在这里插入图片描述

查找路由
  • 消息发送之前,首先需要获取主题的路由信息。如果是第一次,则会从namesrv获取topic元数据,获取后会缓存下来,以后从缓存中获取。

在这里插入图片描述

选择消息队列
  • TopicPublishInfo#selectOneMessageQueue

在这里插入图片描述

批量发送

  • 单条消息发送时,消息体的内容将保存在body中,批量消息发送,需要将多条消息的内容存储在body中,RocketMQ 对多条消息内容进行固定格式进行存储。
  • DefaultMQProducer#send()方法定义如下:

在这里插入图片描述

  • DefaultMQProducer#batch()方法的实现:

在这里插入图片描述

  • 序列化MessageBatch的过程:

在这里插入图片描述

  • 单独一条消息的序列化过程:

在这里插入图片描述

消息存储

文件存储路径

  • 默认存储路径:
public class MessageStoreConfig {
    @ImportantField
    private String storePathRootDir = System.getProperty("user.home") + File.separator + "store";
    // ...
}
  • 可配置:conf/broker.conf
# 存储路径
storePathRootDir=${path}

文件介绍

在这里插入图片描述

  • 下面对上面的文件进行逐一介绍:

CommitLog

  • 一台Broker服务器只有一个CommitLog文件(组),RocketMQ会将所有主题的消息存储在同一个文件中,这个文件中就存储着一条条Message,每条Message都会按照顺序写入。

在这里插入图片描述

存储的内容到底⻓什么样子
  • 消息的格式,即一条消息包含哪些字段;每个字段所占的字节大小。
  • MessageDecoder#decode():

在这里插入图片描述

commitlog特点

在这里插入图片描述

  • CommitLog 目前存储在 MappedFile 有两种内容类型:
    • MESSAGE:消息
    • BLANK:文件不足以存储消息时的空白占位。
  • 关键方法:CommitLog#getMinOffset、CommitLog#rollNextFile、CommitLog#getMessage

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  • 存储消息 CommitLog#putMessage

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

刷盘策略:FlushCommitLogService
  • CommitLog中提交执行的顺序:
// 刷盘服务,进行同步||异步 flush||commit
handleDiskFlush(result, putMessageResult, msg);
  • handleDiskFlush 三种情况:

在这里插入图片描述

线程服务场景插入消息性能
CommitRealTimeService异步刷盘 && 开启内存字节缓冲区第一
FlushRealTimeService异步刷盘 && 关闭内存字节缓冲区第二
GroupCommitService同步刷盘第三

ConsumeQueue

解析文件
  • 为了加速ConsumeQueue消息条目的检索速度和节省磁盘空间,每一个ConsumeQueue条目不会存储消息的全量信息,其格式设计如下图所示:

在这里插入图片描述

消费消息

ConsumeQueue#getIndexBuffer:
在这里插入图片描述

  • 然后拿到消息在 CommitLog#getMessage 文件中的偏移量和消息⻓度,获取消息。

在这里插入图片描述

Index

index索引文件
  • RocketMQ引入Hash索引机制,为消息建立索引,它的键就是Message Key 和 Unique Key。
  • HashMap的设计包括两个基本点:Hash槽与Hash冲突的链表结构。
  • 那么,我们先看看index索引文件的结构

在这里插入图片描述

  • IndexFile组成介绍:从上面的那个图可以看出,IndexFile 总共包含 IndexHeader、 Hash 槽、 Hash 条目(数据)
IndexHeader
  • 头部,包含40字节,记录该IndexFile的统计信息,其结构如下:
    • beginTimestamp:该索引文件中包含消息的最小存储时间。
    • endTimestamp:该索引文件中包含消息的的最大存储时间。
    • beginPhyoffset:该索引文件中包含消息的最小物理偏移量(commitlog文件偏移量)。
    • endPhyoffset:该索引文件中包含消息的最大物理偏移量(commiglog文件偏移量)。
    • hashslotCount:hashslot个数,并不是hash槽使用的个数,在这里意义不大。
    • indexCount:Index条目列表当前已使用的个数,Index条目在Index条目列表中按照顺序存 储。
Hash槽
  • 一个IndexFile默认包含500w个Hash槽。每个Hash槽存储的是落在该Hash槽的hashcode最新的 Index 索引。
Index条目列表
  • 默认一个索引文件包含2000w个条目,每一个Index条目结构如下:
    • hashcode:key的hashcode。
    • phyoffset:消息对应的物理偏移量。
    • timedif:该消息存储时间和第一条消息的时间戳的差值,小于0该消息无效。
    • preIndexNo:该条目的前一条记录的Index索引,当出现hash冲突的时候,构建链表结构。
构建索引
  • 我们发送的消息体中,包含 Message Key 或 Unique Key ,那么就会给它们每一个都构建索引。
  • 这里重点有两个:
    • 根据消息Key计算Hash槽的位置;
    • 根据Hash槽的数量和Index索引来计算Index条目的起始位置。
  • 将当前 Index条目 的索引值,写在Hash槽 absSlotPos 位置上;将Index条目的具体信息(hashcode/消息偏移量/时间差值/hash槽的值) ,从起始偏移量 absIndexPos 开始,顺序按字节写入
  • 参考代码:IndexFile#putKey()

在这里插入图片描述

  • 这样构建完Index索引之后,根据 Message Key 或 Unique Key 查询消息就简单了。
  • 比如通过 RocketMQ 客户端工具,根据 Unique Key 来查询消息。
  • 在 Broker 端,通过 Unique Key 来计算Hash槽的位置,从而找到Index索引数据。从Index索引中拿到消息的物理偏移量,最后根据消息物理偏移量,直接到 CommitLog 文件中去找就可以了。

Checkpoint文件

  • checkpoint的作用是记录Commitlog、ConsumeQueue、Index文件的刷盘时间点,文件固定⻓度为 4K,其中只用该文件的前面24个字节,其存储格式如下图所示:

在这里插入图片描述

  • physicMsgTimestamp:commitLog文件刷盘时间点
  • logicsMsgTimestamp:消息消费队列文件刷盘时间点
  • indexMsgTimestamp:索引文件刷盘时间点

在这里插入图片描述

文件清除机制

  • RocketMQ顺序写Commitlog、ConsumeQueue文件,所有写操作全部落在最后一个 CommitLog 或ConsumeQueue文件上,之前的文件在下一个文件创建后,将不会再被更新。
  • RocketMQ清除过期文件的方法是:如果非当前写文件在一定时间间隔内没有再次被更新,则认为是过期文件,可以被删除,RocketMQ不会管这个这个文件上的消息是否被全部消费。默认每个文件的过期时间为72小时。通过在Broker配置文件中设置fileReservedTime来改变过期时间,单位为小时
public class MessageStoreConfig {
	private int fileReservedTime = 72;
  	// ...
}

在这里插入图片描述

  • 主要清除CommitLog、ConsumeQueue的过期文件。CommitLog 与 ConsumeQueue 对于过期文件 的删除算法、逻辑大同小异,以 CommitLog 过期文件为例来详细分析其实现原理。
// DefaultMessageStore$CleanCommitLogService
class CleanCommitLogService {
	// ...
	public void run() {
        try {
            // 1、尝试删除过期文件
            this.deleteExpiredFiles();
            // 2、重试删除被hange(由于被其他线程引用在第一阶段未删除的文件),在这里再重试一次
            this.redeleteHangedFile();
        } catch (Throwable e) {
            DefaultMessageStore.log.warn(this.getServiceName() + " service has exception. ", e);
        }
    }
    // ...
}

整个执行过程分为两个大的步骤

  • DefaultMessageStore$CleanCommitLogService#deleteExpiredFiles:在如下三种情况任意满足之一的情况下将继续执行删除文件操作
    • 到了删除文件的时间点,RocketMQ通过deleteWhen设置一天的固定时间执行一次删除过期文件操作,默认为凌晨4点。
    • 判断磁盘空间是否充足,如果不充足,则返回true,表示应该触发过期文件删除操作。
    • 预留,手工触发,可以通过调用excuteDeleteFilesManualy方法手工触发过期文件删除,目前RocketMQ暂未封装手工触发文件删除的命令。

在这里插入图片描述

  • 重点分析一下磁盘不足的判断依据,可以跟进去方法:DefaultMessageStore$CleanCommitLogService#isSpaceToDelete

Consumer

消费者类图

在这里插入图片描述

  • DefaultMQPushConsumer:

在这里插入图片描述

在这里插入图片描述

消费者启动

  • 代码入口,DefaultMQPushConsumer.start()方法

在这里插入图片描述

  • DefaultMQPushConsumerImpl#start:

在这里插入图片描述
在这里插入图片描述

  • 进入DefaultMQPushConsumerImpl.copySubscription()方法:

在这里插入图片描述

  • 第 9 步:加载消息进度
this.offsetStore.load();
  • offsetStore是用来操作消费进度的对象,看一下RemoteBrokerOffsetStore对象,push模式消费进度最后持久化在broker端,但是consumer端在内存中也持有消费进度,RemoteBrokerOffsetStore参数:

在这里插入图片描述

消息的拉取

  • 分析一下PUSH模式下的集群模式消息拉取代码,同一个消费组内有多个消费者,一个topic主题下又有多个消费队列,那么消费者是怎么分配这些消费队列的呢?
  • 从上面的启动的代码中是不是还记得在 org.apache.rocketmq.client.impl.factory.MQClientInstance#start 中,启动了pullMessageService 服务线程,这个服务线程的作用就是拉取消息,看下他的run方法:

在这里插入图片描述

  • 从pullRequestQueue中获取pullRequest,如果pullRequestQueue为空,那么线程将阻塞直到有 pullRequest 放入,那么pullRequest 是什么时候放入的呢,有两个地方:

在这里插入图片描述

  • 看下PullRequest类:

在这里插入图片描述

  • 如果从pullRequestQueue中take到pullRequest,那么执行this.pullMessage(pullRequest);

在这里插入图片描述

  • 再进入DefaultMQPushConsumerImpl.pullMessage()方法,这个方法就是整个消息拉取的关键方法:

在这里插入图片描述

  • 这里想说一下很多地方都用到了状态,是否停止,暂停这样的属性,一般都是用volatile去修饰,在不同线程中起到通信的作用。

在这里插入图片描述

  • 上面的6、7、8步都在进行流控判断,防止消费端压力太大,未消费消息太多。
  • 第 9 步:获取主题订阅信息

在这里插入图片描述

  • 这里通过pullRequest的messageQueue获取topic,再从rebalanceImpl中通过topic获取 SubscriptionData,作用是去broker端拉取消息的时候,broker端要知道拉取哪个topic下的信息,过滤tag是什么。

在这里插入图片描述

消息的消费

  • 消息拉取到了之后,消费者要进行消息的消费,消息的消费主要是ConsumeMessageService线程做的,我们先看下ConsumeMessageConcurrentlyService 的构造函数,在这个构造函数中,new了一个名字叫 consumeExecutor 的线程池,在并发消费的模式下,这个线程池也就是消费消息的方式。

在这里插入图片描述

是否从Slave读取

  • Broker 接收到消息消费者拉取请求,在获取本地堆积的消息量后,会计算服务器的消息堆积量是否大于物理内存的一定值。如果是,则标记下次从 Slave服务器拉取,计算 Slave服务器的 Broker Id,并响应给消费者。

在这里插入图片描述

  • DefaultMessageStore#getMessage:

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

讲文明的喜羊羊拒绝pua

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值