省流
长篇预警,内容包含RocketMQ的组件介绍,特性解释,消息收发,请针对想要的内容通过目录快速索引,本文基于 5.x版本
RocketMQ 是什么
先来了解了解 RocketMQ的基础概念
在Apache Rocket MQ的Github仓库页的readme中这样描述到
[Apache RocketMQ]is a distributed messaging and streaming platform with low latency, high performance and reliability, trillion-level capacity and flexible scalability.
It offers a variety of features:
- Messaging patterns including publish/subscribe, request/reply and streaming
- Financial grade transactional message
- Built-in fault tolerance and high availability configuration options base on DLedger Controller
- Built-in message tracing capability, also support opentracing Versatile big-data and streaming ecosystem integration
- Message retroactivity by time or offset
- Reliable FIFO and strict ordered messaging in the same queue Efficient pull and push consumption model
- Million-level message accumulation capacity in a single queue
- Multiple messaging protocols like gRPC, MQTT, JMS and OpenMessaging Flexible distributed scale-out deployment architecture
- Lightning-fast batch message exchange system
- Various message filter mechanics such as SQL and Tag
- Docker images for isolated testing and cloud isolated clusters
- Feature-rich administrative dashboard for configuration, metrics and monitoring
- Authentication and authorization Free open source connectors, for both sources and sinks
- Lightweight real-time computing
Apache RocketMQ是一个分布式消息和流媒体平台,具有低延迟、高性能和可靠性、万亿级容量和灵活的可扩展性。
提供了各种各样的消息能力
……
RocketMQ的部署模型
从图上可知,生产者通过NameServer找到Broker的地址,通过Broker去写消息,而消费者也是通过NameServer获取Broker的信息,从Broker获取数据
名字服务器 NameServer
定义
NameServer是一个简单的 Topic 路由注册中心,支持 Topic、Broker 的动态注册与发现。
功能
主要包括两个功能:
- Broker管理,NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活;
- 路由信息管理,每个NameServer将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息。Producer和Consumer通过NameServer就可以知道整个Broker集群的路由信息,从而进行消息的投递和消费。
NameServer通常会有多个实例部署,各实例间相互不进行信息通讯。Broker是向每一台NameServer注册自己的路由信息,所以每一个NameServer实例上面都保存一份完整的路由信息。当某个NameServer因某种原因下线了,客户端仍然可以向其它NameServer获取路由信息。
代理服务器 Broker
Broker主要负责消息的存储、投递和查询以及服务高可用保证。
NameServer几乎无状态节点,因此可集群部署,节点之间无任何信息同步。Broker部署相对复杂。
在 Master-Slave 架构中,Broker 分为 Master 与 Slave。一个Master可以对应多个Slave,但是一个Slave只能对应一个Master。Master 与 Slave 的对应关系通过指定相同的BrokerName,不同的BrokerId 来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。
部署模型总结
- 每个 Broker 与 NameServer 集群中的所有节点建立长连接,定时注册 Topic 信息到所有 NameServer。
- Producer 与 NameServer 集群中的其中一个节点建立长连接,定期从 NameServer 获取Topic路由信息,并向提供 Topic 服务的 Master 建立长连接,且定时向 Master 发送心跳。Producer 完全无状态。
- Consumer 与 NameServer 集群中的其中一个节点建立长连接,定期从 NameServer 获取 Topic 路由信息,并向提供 Topic 服务的 Master、Slave 建立长连接,且定时向 Master、Slave发送心跳。Consumer 既可以从 Master 订阅消息,也可以从Slave订阅消息。
RocketMQ的领域模型
主题
TOPIC的定义
主题是 Apache RocketMQ 中消息传输和存储的顶层容器,用于标识同一类业务逻辑的消息。 主题的作用主要如下:
- 定义数据的分类隔离: 在 Apache RocketMQ 的方案设计中,建议将不同业务类型的数据拆分到不同的主题中管理,通过主题实现存储的隔离性和订阅隔离性。
- 定义数据的身份和权限: Apache RocketMQ 的消息本身是匿名无身份的,同一分类的消息使用相同的主题来做身份识别和权限管理。
主题是 Apache RocketMQ 的顶层存储,所有消息资源的定义都在主题内部完成,但主题是一个逻辑概念,并不是实际的消息容器。
主题内部由多个队列组成,消息的存储和水平扩展能力最终是由队列实现的;并且针对主题的所有约束和属性设置,最终也是通过主题内部的队列来实现。
主题(TOPIC)的属性
属性名 | 定义 | 取值 | 约束 |
---|---|---|---|
主题名称 | 主题的名称,用于标识主题,主题名称集群内全局唯一。 | 由用户创建主题时定义。 | 详见参数约束和建议中的Topic名称 |
队列列表 | 队列作为主题的组成单元,是消息存储的实际容器,一个主题内包含一个或多个队列,消息实际存储在主题的各队列内 | 系统根据队列数量给主题分配队列,队列数量创建主题时定义。 | 一个主题内至少包含一个队列。 |
消息类型 | 主题所支持的消息类型。 | 创建主题时选择消息类型。Normal/FIFO/Delay/Transaction | 从5.0版本开始,支持强制校验消息类型,即每个主题只允许发送一种消息类型的消息,强制校验功能默认开启 |
队列
队列的定义
队列是 Apache RocketMQ 中消息存储和传输的实际容器,也是 Apache RocketMQ 消息的最小存储单元。 Apache RocketMQ 的所有主题都是由多个队列组成,以此实现队列数量的水平拆分和队列内部的流式存储。
队列的主要作用如下:
-
存储顺序性
队列天然具备顺序性,即消息按照进入队列的顺序写入存储,同一队列间的消息天然存在顺序关系,队列头部为最早写入的消息,队列尾部为最新写入的消息。消息在队列中的位置和消息之间的顺序通过位点(Offset)进行标记管理。
-
流式操作语义
Apache RocketMQ 基于队列的存储模型可确保消息从任意位点读取任意数量的消息,以此实现类似聚合读取、回溯读取等特性,这些特性是RabbitMQ、ActiveMQ等非队列存储模型不具备的。
队列有以下特性
-
Apache RocketMQ 默认提供消息可靠存储机制,所有发送成功的消息都被持久化存储到队列中,配合生产者和消费者客户端的调用可实现至少投递一次的可靠性语义。
-
Apache RocketMQ 队列模型和Kafka的分区(Partition)模型类似。在 Apache RocketMQ 消息收发模型中,队列属于主题的一部分,虽然所有的消息资源以主题粒度管理,但实际的操作实现是面向队列。例如,生产者指定某个主题,向主题内发送消息,但实际消息发送到该主题下的某个队列中。
-
Apache RocketMQ 中通过修改队列数量,以此实现横向的水平扩容和缩容。
队列的内部属性
-
定义:当前队列是否可以读写数据。
-
取值:由服务端定义,枚举值如下
- 6:读写状态,当前队列允许读取消息和写入消息。
- 4:只读状态,当前队列只允许读取消息,不允许写入消息。
- 2:只写状态,当前队列只允许写入消息,不允许读取消息。
- 0:不可读写状态,当前队列不允许读取消息和写入消息。
该行为影响队列业务状态,不建议频繁修改,可在进行负载时进行读写分离限制
消息
消息的定义
消息是 Apache RocketMQ 中的最小数据传输单元。生产者将业务数据的负载和拓展属性包装成消息发送到 Apache RocketMQ 服务端,服务端按照相关语义将消息投递到消费端进行消费。
Apache RocketMQ 的消息模型具备如下特点
:
-
消息不可变性 消息本质上是已经产生并确定的事件,一旦产生后,消息的内容不会发生改变。即使经过传输链路的控制也不会发生变化,消费端获取的消息都是只读消息视图。
-
消息持久化 Apache RocketMQ 会默认对消息进行持久化,即将接收到的消息存储到 Apache RocketMQ 服务端的存储文件中,保证消息的可回溯性和系统故障场景下的可恢复性。
消息参与整个PV的过程
- 消息由生产者初始化并发送到Apache RocketMQ 服务端。
- 消息按照到达Apache RocketMQ 服务端的顺序存储到队列中。
- 消费者按照指定的订阅关系从Apache RocketMQ 服务端中获取消息并消费。
消息类型
Apache RocketMQ 支持的消息类型如下:
-
Normal:[普通消息],Apache RocketMQ基本消息功能,支持生产者和消费者的异步解耦通信。普通消息生命周期:
- 初始化:消息被生产者构建并完成初始化,待发送到服务端的状态。
- 待消费:消息被发送到服务端,对消费者可见,等待消费者消费的状态。
- 消费中:消息被消费者获取,并按照消费者本地的业务逻辑进行处理的过程。 此时服务端会等待消费者完成消费并提交消费结果,如果一定时间后没有收到消费者的响应,Apache RocketMQ会对消息进行重试处理。。
- 消费提交:消费者完成消费处理,并向服务端提交消费结果,服务端标记当前消息已经被处理(包括消费成功和失败)。 Apache RocketMQ默认支持保留所有消息,此时消息数据并不会立即被删除,只是逻辑标记已消费。消息在保存时间到期或存储空间不足被删除前,消费者仍然可以回溯消息重新消费。
- 消息删除:Apache RocketMQ按照消息保存机制滚动清理最早的消息数据,将消息从物理文件中删除。
-
FIFO:[顺序消息],Apache RocketMQ 提供的一种高级消息类型,支持消费者按照发送消息的先后顺序获取消息,从而实现业务场景中的顺序处理。 相比其他类型消息,顺序消息在发送、存储和投递的处理过程中,更多强调多条消息间的先后顺序关系。Apache RocketMQ 顺序消息的顺序关系通过消息组(MessageGroup)判定和识别,发送顺序消息时需要为每条消息设置归属的消息组,相同消息组的多条消息之间遵循先进先出的顺序关系,不同消息组、无消息组的消息之间不涉及顺序性。保证消息的顺序性分为两部分:
-
生产顺序性,Apache RocketMQ 通过生产者和服务端的协议保障单个生产者串行地发送消息,并按序存储和持久化,保证消息生产的顺序性,则必须满足以下条件
- 单一生产者
- 串行发送
服务端顺序存储逻辑如下:
- 相同消息组的消息按照先后顺序被存储在同一个队列。
- 不同消息组的消息可以混合在同一个队列中,且不保证连续。
-
消费顺序性,Apache RocketMQ 通过消费者和服务端的协议保障消息消费严格按照存储的先后顺序来处理,保证消息消费的顺序性,则必须满足以下条件
- 投递顺序:需要严格按照接收---处理---应答的语义处理消息,避免因异步处理导致消息乱序
- 有限重试:一条消息如果一直重试失败,超过最大重试次数后将不再重试,跳过这条消息消费,不会一直阻塞后续消息处理。
-
顺序消息生命周期
- 初始化:消息被生产者构建并完成初始化,待发送到服务端的状态。
- 待消费:消息被发送到服务端,对消费者可见,等待消费者消费的状态。
- 消费中:消息被消费者获取,并按照消费者本地的业务逻辑进行处理的过程。 此时服务端会等待消费者完成消费并提交消费结果,如果一定时间后没有收到消费者的响应,Apache RocketMQ会对消息进行重试处理。
- 消费提交:消费者完成消费处理,并向服务端提交消费结果,服务端标记当前消息已经被处理(包括消费成功和失败)。 Apache RocketMQ 默认支持保留所有消息,此时消息数据并不会立即被删除,只是逻辑标记已消费。消息在保存时间到期或存储空间不足被删除前,消费者仍然可以回溯消息重新消费。
- 消息删除:Apache RocketMQ按照消息保存机制滚动清理最早的消息数据,将消息从物理文件中删除。
-
-
Delay:[定时/延时消息],通消息被发送至服务端后,在指定时间后才能被消费者消费。通过设置一定的定时时间可以实现分布式场景的延时调度触发效果。
- 定时消息生命周期:
- 初始化:消息被生产者构建并完成初始化,待发送到服务端的状态。
- 定时中:消息被发送到服务端,和普通消息不同的是,服务端不会直接构建消息索引,而是会将定时消息单独存储在定时存储系统中,等待定时时刻到达。
- 待消费:定时时刻到达后,服务端将消息重新写入普通存储引擎,对下游消费者可见,等待消费者消费的状态。
- 消费中:消息被消费者获取,并按照消费者本地的业务逻辑进行处理的过程。 此时服务端会等待消费者完成消费并提交消费结果,如果一定时间后没有收到消费者的响应,Apache RocketMQ会对消息进行重试处理。
- 消费提交:消费者完成消费处理,并向服务端提交消费结果,服务端标记当前消息已经被处理(包括消费成功和失败)。 Apache RocketMQ 默认支持保留所有消息,此时消息数据并不会立即被删除,只是逻辑标记已消费。消息在保存时间到期或存储空间不足被删除前,消费者仍然可以回溯消息重新消费。
- 消息删除:Apache RocketMQ按照消息保存机制滚动清理最早的消息数据,将消息从物理文件中删除。
- 定时消息生命周期:
-
Transaction:[事务消息],Apache RocketMQ 支持分布式事务消息,支持在分布式场景下保障消息生产和本地事务的最终一致性。
- 事务消息处理流程
-
生产者将消息发送至Apache RocketMQ服务端。
-
Apache RocketMQ服务端将消息持久化成功之后,向生产者返回Ack确认消息已经发送成功,此时消息被标记为"暂不能投递",这种状态下的消息即为半事务消息。
-
生产者开始执行本地事务逻辑。
-
生产者根据本地事务执行结果向服务端提交二次确认结果(Commit或是Rollback),服务端收到确认结果后处理逻辑如下:
- 二次确认结果为Commit:服务端将半事务消息标记为可投递,并投递给消费者。
- 二次确认结果为Rollback:服务端将回滚事务,不会将半事务消息投递给消费者。
-
在断网或者是生产者应用重启的特殊情况下,若服务端未收到发送者提交的二次确认结果,或服务端收到的二次确认结果为Unknown未知状态,经过固定时间后,服务端将对消息生产者即生产者集群中任一生产者实例发起消息回查。
-
生产者收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
-
生产者根据检查到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤4对半事务消息进行处理。
-
- 事务消息的生命周期
- 初始化:半事务消息被生产者构建并完成初始化,待发送到服务端的状态。
- 事务待提交:半事务消息被发送到服务端,和普通消息不同,并不会直接被服务端持久化,而是会被单独存储到事务存储系统中,等待第二阶段本地事务返回执行结果后再提交。此时消息对下游消费者不可见。
- 消息回滚:第二阶段如果事务执行结果明确为回滚,服务端会将半事务消息回滚,该事务消息流程终止。
- 提交待消费:第二阶段如果事务执行结果明确为提交,服务端会将半事务消息重新存储到普通存储系统中,此时消息对下游消费者可见,等待被消费者获取并消费。
- 消费中:消息被消费者获取,并按照消费者本地的业务逻辑进行处理的过程。 此时服务端会等待消费者完成消费并提交消费结果,如果一定时间后没有收到消费者的响应,Apache RocketMQ会对消息进行重试处理。
- 消费提交:消费者完成消费处理,并向服务端提交消费结果,服务端标记当前消息已经被处理(包括消费成功和失败)。 Apache RocketMQ默认支持保留所有消息,此时消息数据并不会立即被删除,只是逻辑标记已消费。消息在保存时间到期或存储空间不足被删除前,消费者仍然可以回溯消息重新消费。
- 消息删除:Apache RocketMQ按照消息保存机制滚动清理最早的消息数据,将消息从物理文件中删除。
- 事务消息处理流程
消息的属性
名称 | 定义 | 取值 |
---|---|---|
主题名称 | 当前消息所属的主题的名称。集群内全局唯一。 | |
消息类型 | 当前消息的类型。 | Normal/FIFO/Delay/Transaction |
消息队列 | 实际存储当前消息的队列。 | 由服务端指定并填充。 |
消息位点 | 前消息存储在队列中的位置。 | 由服务端指定并填充。取值范围:0~long.Max。 |
消息ID | 消息的唯一标识,集群内每条消息的ID全局唯一。 | 生产者客户端系统自动生成。固定为数字和大写字母组成的32位字符串。 |
索引Key列表(可选) | 消息的索引键,可通过设置不同的Key区分消息和快速查找消息。 | 由服务端指定并填充。取值范围:0~long.Max。 |
过滤标签Tag(可选) | 消息的过滤标签。消费者可通过Tag对消息进行过滤,仅接收指定标签的消息。 | 由生产者客户端定义 |
定时时间(可选) | 定时场景下,消息触发延时投递的毫秒级时间戳。 | 由消息生产者定义。 |
消息发送时间 | 消息发送时,生产者客户端系统的本地毫秒级时间戳。 | 由生产者客户端系统填充。 |
消息保存时间戳 | 消息在Apache RocketMQ 服务端完成存储时,服务端系统的本地毫秒级时间戳。 对于定时消息和事务消息,消息保存时间指的是消息生效对消费方可见的服务端系统时间。 | 由服务端系统填充 |
消费重试次数 | 消息消费失败后,Apache RocketMQ 服务端重新投递的次数。每次重试后,重试次数加1。 | 由服务端系统标记。首次消费,重试次数为0;消费失败首次重试时,重试次数为1。 |
业务自定义属性 | 生产者可以自定义设置的扩展信息。 | 由消息生产者自定义,按照字符串键值对设置。 |
消息负载 | 业务消息的实际报文数据。 | 由生产者负责序列化编码,按照二进制字节传输。 |
生产者
生产者定义
生产者是 Apache RocketMQ 系统中用来构建并传输消息到服务端的运行实体。
在消息生产者中,可以定义如下传输行为:
- 发送方式:生产者可通过API接口设置消息发送的方式。Apache RocketMQ 支持同步传输和异步传输。
- 批量发送:生产者可通过API接口设置消息批量传输的方式。例如,批量发送的消息条数或消息大小。
- 事务行为:Apache RocketMQ 支持事务消息,对于事务消息需要生产者配合进行事务检查等行为保障事务的最终一致性。
生产者和主题的关系为多对多关系,即同一个生产者可以向多个主题发送消息,对于平台类场景如果需要发送消息到多个主题,并不需要创建多个生产者;同一个主题也可以接收多个生产者的消息,以此可以实现生产者性能的水平扩展和容灾。
生产者的属性
名称 | 定义 | 取值 |
---|---|---|
客户端ID | 生产者客户端的标识,用于区分不同的生产者。集群内全局唯一。 | 客户端ID由Apache RocketMQ 的SDK自动生成,主要用于日志查看、问题定位等运维场景,不支持修改 。 |
接入点信息(必选) | 连接服务端的接入地址,用于识别服务端集群。 | 接入点必须按格式配置,建议使用域名,避免使用IP地址,防止节点变更无法进行热点迁移。 |
身份认证信息(可选) | 客户端用于身份验证的凭证信息。 | 仅在服务端开启身份识别和认证时需要传输。 |
请求超时时间(可选) | 客户端网络请求调用的超时时间。 | 取值范围和默认值。 |
预绑定主题列表 | Apache RocketMQ 的生产者需要将消息发送到的目标主题列表 | 事务消息时 必须设置预绑定的消息列表,因为生产者在故障、重启恢复时,需要检查事务消息的主题中是否有未提交的事务,保障业务正常执行。非事务消息时建议设置,服务端会在生产者初始化时根据预绑定主题列表,检查目标主题的访问权限和合法性,提升效率。 |
事务检查器 | Apache RocketMQ 的事务消息机制中,为保证异常场景下事务的最终一致性,生产者需要主动实现事务检查器的接口。 | 发送事务消息时,事务检查器必须设置,且需要和预绑定主题列表一起配合使用。 |
发送重试策略 | 生产者在消息发送失败时的重试策略。 | 指数退避算法参数:INITIAL_BACKOFF /MULTIPLIER /JITTER /MAX_BACKOFF /MIN_CONNECT_TIMEOUT |
消费者组
消费者组定义
消费者分组是 Apache RocketMQ 系统中承载多个消费行为一致的消费者的负载均衡分组。
和消费者不同,消费者分组并不是运行实体,而是一个逻辑资源。在 Apache RocketMQ 中,通过消费者分组内初始化多个消费者实现消费性能的水平扩展以及高可用容灾。
在消费者分组中,统一定义以下消费行为,同一分组下的多个消费者将按照分组内统一的消费行为和负载均衡策略消费消息。
- 订阅关系:Apache RocketMQ 以消费者分组的粒度管理订阅关系,实现订阅关系的管理和追溯。
- 投递顺序性:Apache RocketMQ 的服务端将消息投递给消费者消费时,支持顺序投递和并发投递,投递方式在消费者分组中统一配置。
- 消费重试策略: 消费者消费消息失败时的重试策略,包括重试次数、死信队列设置等。
消费者组的属性
名称 | 定义 | 取值 |
---|---|---|
消费者分组名称 | 消费者分组的名称,用于区分不同的消费者分组。集群内全局唯一。 | 消费者分组由用户 设置并创建。 |
投递顺序性 | 消费者消费消息时,Apache RocketMQ 向消费者客户端投递消息的顺序。 | 默认投递方式为并发投递。 |
消费重试策略 | 消费者消费消息失败时,系统的重试策略。消费者消费消息失败时,系统会按照重试策略,将指定消息投递给消费者重新消费。 | 重试策略包括:最大重试次数,重试间隔,需要注意的是重试间隔仅在PushConsumer消费类型下有效。 |
订阅关系 | 当前消费者分组关联的订阅关系集合。 | TAG过滤\SQL92过滤 |
消费者
消费者定义
消费者是 Apache RocketMQ 中用来接收并处理消息的运行实体。 消费者通常被集成在业务系统中,从 Apache RocketMQ 服务端获取消息,并将消息转化成业务可理解的信息,供业务逻辑处理。
在消息消费端,可以定义如下传输行为:
- 消费者身份:消费者必须关联一个指定的消费者分组,以获取分组内统一定义的行为配置和消费状态。
- 消费者类型:Apache RocketMQ 面向不同的开发场景提供了多样的消费者类型,包括PushConsumer类型、SimpleConsumer类型、PullConsumer类型(仅推荐流处理场景使用)等。
- 消费者本地运行配置:消费者根据不同的消费者类型,控制消费者客户端本地的运行配置。例如消费者客户端的线程数,消费并发度等,实现不同的传输效果。
消费者的属性
名称 | 定义 | 取值 |
---|---|---|
消费者分组名称 | 当前消费者关联的消费者分组名称,消费者必须关联到指定的消费者分组,通过消费者分组获取消费行为。 | 消费者分组为Apache RocketMQ 的逻辑资源,需要您提前通过控制台或OpenAPI创建。 |
客户端ID | 消费者客户端的标识,用于区分不同的消费者。集群内全局唯一。 | 客户端ID由Apache RocketMQ 的SDK自动生成,主要用于日志查看、问题定位等运维场景,不支持修改。 |
接入点信息(必选) | 连接服务端的接入地址,用于识别服务端集群。 | 接入点必须按格式配置,建议使用域名,避免使用IP地址,防止节点变更无法进行热点迁移。 |
身份认证信息(可选) | 客户端用于身份验证的凭证信息。 | 仅在服务端开启身份识别和认证时需要传输。 |
请求超时时间(可选 | 客户端网络请求调用的超时时间。 | 默认值:3000毫秒。取值范围建议不要超过30000毫秒。 |
预绑定订阅关系列表 | 指定消费者的订阅关系列表。初始化阶段对目标主题进行权限及合法性校验,无需等到应用启动后才能校验。 | 建议在消费者初始化阶段明确订阅关系即要订阅的主题列表,若未设置,或订阅的主题动态变更,Apache RocketMQ 会对目标主题进行动态补充校验。 |
消费监听器 | 服务端将消息推送给消费者后,消费者调用消息消费逻辑的监听器。 | 由消费者客户端本地配置,PushConsumer类型的消费者消费消息必须设置 |
总结
以上概述来自于RocketMQ官网
大概的,简要的说,工作流程如下图所示
大概的就是
- 生产消息:生产者通过nameserver获取topic所在的broker,然后生产者实例化消息示例发送给对应的broker去写入到对应的queue
- 消费消息:消费者通过nameserver获取topic所在的broker,通过broker去获取queue中的消息
- broker:管理queue,类似于最终服务节点
- nameserver: broker调度和注册中心,负责topic的路由和broker健康管理
RocketMQ怎么进行工作的
Quick Start
在官网的快速启动中,我们看到了通过maven引入依赖从而接入RocketMQ的示例
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client-java</artifactId>
<version>${rocketmq-client-java-version}</version>
</dependency>
内部工作流程
我们直接从源码看起,我们在apache的仓库中找到rocketmq-client
Topic路由获取
从官网的提供的业务流程来看,发送前会先从nameserver获取topic,topic在客户端是怎么进行维护的呢,
我们知道的是生产者
和消费者
都是一个client,我们来看看ClientImpl
的定义
public abstract class ClientImpl extends AbstractIdleService implements Client, ClientSessionHandler,
MessageInterceptor {
先补个知识点
AbstractIdleService 来自guava中的concurrent包,提供concurrent包下Sevice接口的简单实现,Sevice接口封装一个服务对象的运行状态、包括start和stop等方法
客户端创建时获取
ClientImpl实现了AbstractIdleService,查看源码可以知道,build完生产者消费者的实例会调用他的#startAsync()
方法,我们来看看ClientImpl中实现得startUp()方法
/**
* Start the rocketmq client and do some preparatory work.
*/
@Override
protected void startUp() throws Exception {
log.info("Begin to start the rocketmq client, clientId={}", clientId);
this.clientManager.startAsync().awaitRunning();
// Fetch topic route from remote.
log.info("Begin to fetch topic(s) route data from remote during client startup, clientId={}, topics={}",
clientId, topics);
for (String topic : topics) {
final ListenableFuture<TopicRouteData> future = fetchTopicRoute(topic);
future.get();
}
log.info("Fetch topic route data from remote successfully during startup, clientId={}, topics={}",
clientId, topics);
// Update route cache periodically.
final ScheduledExecutorService scheduler = clientManager.getScheduler();
this.updateRouteCacheFuture = scheduler.scheduleWithFixedDelay(() -> {
try {
updateRouteCache();
} catch (Throwable t) {
log.error("Exception raised while updating topic route cache, clientId={}", clientId, t);
}
}, 10, 30, TimeUnit.SECONDS);
log.info("The rocketmq client starts successfully, clientId={}", clientId);
}
从源码中来看,会先根据client中定义的topic去调用fetchTopicRoute来获取topic对应的路由,具体的就是通过grpc向nameserver获取,方法名为
private ListenableFuture<TopicRouteData> fetchTopicRoute(final String topic) {
final ListenableFuture<TopicRouteData> future0 = fetchTopicRoute0(topic);
final ListenableFuture<TopicRouteData> future = Futures.transformAsync(future0,
topicRouteData -> onTopicRouteDataFetched(topic, topicRouteData), MoreExecutors.directExecutor());
Futures.addCallback(future, new FutureCallback<TopicRouteData>() {
@Override
public void onSuccess(TopicRouteData topicRouteData) {
log.info("Fetch topic route successfully, clientId={}, topic={}, topicRouteData={}", clientId,
topic, topicRouteData);
}
@Override
public void onFailure(Throwable t) {
log.error("Failed to fetch topic route, clientId={}, topic={}", clientId, topic, t);
}
}, MoreExecutors.directExecutor());
return future;
}
protected ListenableFuture<TopicRouteData> fetchTopicRoute0(final String topic) {
Resource topicResource = Resource.newBuilder()
.setResourceNamespace(clientConfiguration.getNamespace())
.setName(topic)
.build();
final QueryRouteRequest request = QueryRouteRequest.newBuilder().setTopic(topicResource)
.setEndpoints(endpoints.toProtobuf()).build();
final RpcFuture<QueryRouteRequest, QueryRouteResponse> future =
clientManager.queryRoute(endpoints, request, clientConfiguration.getRequestTimeout());
return Futures.transformAsync(future, response -> {
final Status status = response.getStatus();
StatusChecker.check(status, future);
final List<MessageQueue> messageQueuesList = response.getMessageQueuesList();
final TopicRouteData topicRouteData = new TopicRouteData(messageQueuesList);
return Futures.immediateFuture(topicRouteData);
}, MoreExecutors.directExecutor());
}
获取完成后调用#onTopicRouteDataFetched
方法更新topicRouteCache
/**
* Triggered when {@link TopicRouteData} is fetched from remote.
*/
public ListenableFuture<TopicRouteData> onTopicRouteDataFetched(String topic,
TopicRouteData topicRouteData) throws ClientException {
final Set<Endpoints> routeEndpoints = topicRouteData
.getMessageQueues().stream()
.map(mq -> mq.getBroker().getEndpoints())
.collect(Collectors.toSet());
final Set<Endpoints> existRouteEndpoints = getTotalRouteEndpoints();
final Set<Endpoints> newEndpoints = new HashSet<>(Sets.difference(routeEndpoints, existRouteEndpoints));
List<ListenableFuture<?>> futures = new ArrayList<>();
for (Endpoints endpoints : newEndpoints) {
final ClientSessionImpl clientSession = getClientSession(endpoints);
futures.add(clientSession.syncSettings());
}
final ListenableFuture<?> future = Futures.allAsList(futures);
return Futures.transform(future, (Function<Object, TopicRouteData>) input -> {
topicRouteCache.put(topic, topicRouteData);
onTopicRouteDataUpdate0(topic, topicRouteData);
return topicRouteData;
}, MoreExecutors.directExecutor());
}
后续同步获取
在第一次同步完成后,还注册了个定时任务,在10秒后每30秒调用一次#updateRouteCache
方法去更新topic路由,该方法也是同样调用#fetchTopicRoute
方法
private void updateRouteCache() {
log.info("Start to update route cache for a new round, clientId={}", clientId);
topicRouteCache.keySet().forEach(topic -> {
final ListenableFuture<TopicRouteData> future = fetchTopicRoute(topic);
Futures.addCallback(future, new FutureCallback<TopicRouteData>() {
@Override
public void onSuccess(TopicRouteData topicRouteData) {
}
@Override
public void onFailure(Throwable t) {
log.error("Failed to fetch topic route for update cache, topic={}, clientId={}", topic,
clientId, t);
}
}, MoreExecutors.directExecutor());
});
}
总结
client创建时,会先获取一次topic的路由信息,而后会延迟10秒钟
开始定时任务,每隔30秒
获取一次topic路由数据对client的路由缓存进行更新
发送消息
从官网Quicke start中可以看到生产者的示例
package org.apache.rocketmq.client.java.example;
import org.apache.rocketmq.client.apis.ClientConfiguration;
import org.apache.rocketmq.client.apis.ClientConfigurationBuilder;
import org.apache.rocketmq.client.apis.ClientException;
import org.apache.rocketmq.client.apis.ClientServiceProvider;
import org.apache.rocketmq.client.apis.message.Message;
import org.apache.rocketmq.client.apis.producer.Producer;
import org.apache.rocketmq.client.apis.producer.SendReceipt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ProducerExample {
private static final Logger logger = LoggerFactory.getLogger(ProducerExample.class);
public static void main(String[] args) throws ClientException {
// 接入点地址,需要设置成Proxy的地址和端口列表,一般是xxx:8081;xxx:8081。
String endpoint = "localhost:8081";
// 消息发送的目标Topic名称,需要提前创建。
String topic = "TestTopic";
ClientServiceProvider provider = ClientServiceProvider.loadService();
ClientConfigurationBuilder builder = ClientConfiguration.newBuilder().setEndpoints(endpoint);
ClientConfiguration configuration = builder.build();
// 初始化Producer时需要设置通信配置以及预绑定的Topic。
Producer producer = provider.newProducerBuilder()
.setTopics(topic)
.setClientConfiguration(configuration)
.build();
// 普通消息发送。
Message message = provider.newMessageBuilder()
.setTopic(topic)
// 设置消息索引键,可根据关键字精确查找某条消息。
.setKeys("messageKey")
// 设置消息Tag,用于消费端根据指定Tag过滤消息。
.setTag("messageTag")
// 消息体。
.setBody("messageBody".getBytes())
.build();
try {
// 发送消息,需要关注发送结果,并捕获失败等异常。
SendReceipt sendReceipt = producer.send(message);
logger.info("Send message successfully, messageId={}", sendReceipt.getMessageId());
} catch (ClientException e) {
logger.error("Failed to send message", e);
}
// producer.close();
}
}
从上面的示例中,我们可以看到他的流程是
- 通过
ClientServiceProvider
通过spi加载客户端服务ClientServiceProvider
- 构建客户端配置信息
ClientConfiguration
- 设置
topic
和ClientConfiguration
构建生产者Producer
- 构建
Message
并设置消息的topic
、keys
、tag
、body
- 调用生产者的
#send()
并获取服务端收据
从构建配置信息开始
ClientConfigurationBuilder builder = ClientConfiguration.newBuilder().setEndpoints(endpoint);
ClientConfiguration configuration = builder.build();
源码中是这样定义的,我们来看看builder的源码
/**
* Finalize the build of {@link ClientConfiguration}.
*
* @return the client configuration builder instance.
*/
public ClientConfiguration build() {
checkNotNull(endpoints, "endpoints should not be null");
checkNotNull(requestTimeout, "requestTimeout should not be null");
return new ClientConfiguration(endpoints, sessionCredentialsProvider, requestTimeout, sslEnabled, namespace);
}
具体的流程是,校验数据是否合法后,通过全参构造函数构造ClientConfiguration
对象
public class ClientConfiguration {
//接入点
private final String endpoints;
//会话凭证提供者
private final SessionCredentialsProvider sessionCredentialsProvider;
//请求超时,默认3s
private final Duration requestTimeout;
//ssl开关
private final boolean sslEnabled;
//命名空间
private final String namespace;
……
构建生产者
构建完配置信息对象后,通过ClientServiceProvider
的ProducerBuilder
构建生产者Builder并配置相关的信息后调用#build()
/**
* @see ProducerBuilder#build()
*/
@Override
public Producer build() {
checkNotNull(clientConfiguration, "clientConfiguration has not been set yet");
final ProducerImpl producer = new ProducerImpl(clientConfiguration, topics, maxAttempts, checker);
producer.startAsync().awaitRunning();
return producer;
}
具体的build流程是确认配置信息后构建producer线程,并调用他的#startAsync
方法启动,来看看producer的具体实现
class ProducerImpl extends ClientImpl implements Producer {
//日志
private static final Logger log = LoggerFactory.getLogger(ProducerImpl.class);
//发布设置
protected final PublishingSettings publishingSettings;
//topic路由
final ConcurrentMap<String/* topic */, PublishingLoadBalancer> publishingRouteDataCache;
//事务处理器
private final TransactionChecker checker;
……
其中ClientImpl
是个抽象类,继承了AbstractIdleService
,实现Client、Client会话处理器ClientSessionHandler
,消息拦截器ClientSessionHandler
public abstract class ClientImpl extends AbstractIdleService implements Client, ClientSessionHandler,
MessageInterceptor {
来看看他的实例化方法
ProducerImpl(ClientConfiguration clientConfiguration, Set<String> topics, int maxAttempts,
TransactionChecker checker) {
//调用ClientImpl#ClientImpl(ClientConfiguration clientConfiguration, Set<String> topics)方法
super(clientConfiguration, topics);
//重试处理器
ExponentialBackoffRetryPolicy retryPolicy = ExponentialBackoffRetryPolicy.immediatelyRetryPolicy(maxAttempts);
//配置发布处理器
this.publishingSettings = new PublishingSettings(clientConfiguration.getNamespace(), clientId, endpoints,
retryPolicy, clientConfiguration.getRequestTimeout(), topics);
this.checker = checker;
this.publishingRouteDataCache = new ConcurrentHashMap<>();
}
来看看ClientImpl#ClientImpl
方法
public ClientImpl(ClientConfiguration clientConfiguration, Set<String> topics) {
//校验client的配置信息不能为空
this.clientConfiguration = checkNotNull(clientConfiguration, "clientConfiguration should not be null");
//配置接入点(解析配置的接入点的地址、网络协议)
this.endpoints = new Endpoints(clientConfiguration.getEndpoints());
//配置主题
this.topics = topics;
// 生成客户端id 规则 : 地址@pid@Client的index@创建时间
this.clientId = new ClientId();
//初始化主题路由管理相关
this.topicRouteCache = new ConcurrentHashMap<>();
this.inflightRouteFutureTable = new ConcurrentHashMap<>();
this.inflightRouteFutureLock = new ReentrantLock();
//初始化会话管理相关
this.sessionsTable = new HashMap<>();
this.sessionsLock = new ReentrantReadWriteLock();
//接入点隔离机制
this.isolated = Collections.newSetFromMap(new ConcurrentHashMap<>());
//client管理器,配置定时器线程池、工作线程池
this.clientManager = new ClientManagerImpl(this);
final long clientIdIndex = clientId.getIndex();
//client回调线程池
this.clientCallbackExecutor = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(),
Runtime.getRuntime().availableProcessors(),
60,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
new ThreadFactoryImpl("ClientCallbackWorker", clientIdIndex));
//初始化度量器
this.clientMeterManager = new ClientMeterManager(clientId, clientConfiguration);
//初始化消息拦截器
this.compositedMessageInterceptor =
new CompositedMessageInterceptor(Collections.singletonList(new MessageMeterInterceptor(this,
clientMeterManager)));
//初始化命令线程池
this.telemetryCommandExecutor = new ThreadPoolExecutor(
1,
1,
60,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
new ThreadFactoryImpl("CommandExecutor", clientIdIndex));
}
至此,生产者就实例化完成,实例化完成后,Producer实现了guava的Service接口,调用其#startAsync
方法启动服务,并等到服务开始运行后再返回生产者实例
构建消息
从官网的生产者示例中,我们可以看到消息是通过MessageBuilder#build()
来的,我们来看看源码是怎么实现的
/**
* See {@link MessageBuilder#build()}
*/
@Override
public Message build() {
checkNotNull(topic, "topic has not been set yet");
checkNotNull(body, "body has not been set yet");
return new MessageImpl(topic, body, tag, keys, messageGroup, deliveryTimestamp, properties);
}
MessageBuilder#build() 的执行过程是确认topic
和body
实例化消息对象
MessageImpl(String topic, byte[] body, @Nullable String tag, Collection<String> keys,
@Nullable String messageGroup, @Nullable Long deliveryTimestamp,
Map<String, String> properties) {
//主题
this.topic = topic;
//消息体
this.body = body;
//tag
this.tag = tag;
//消息组
this.messageGroup = messageGroup;
//预期的交付时间戳
this.deliveryTimestamp = deliveryTimestamp;
//消息key集合
this.keys = keys;
//消息的配置
this.properties = properties;
}
进行发送
我们先来看看Producer接口的定义
/**
* Producer是一个线程安全的rocketmq客户端,用于发布消息。
*
* 由于网络超时或其他原因,生产者只承诺至少一次语义。对于生产者来说,“至少一次”语义意味着可能会尝试发送它,消息可能会被复制,但不会丢失。
* 特别是,使用#send(Message, Transaction)时,可能不会进行尝试
*/
public interface Producer extends Closeable {
Producer接口定义了消息一些操作
- 发送消息
SendReceipt send(Message message) throws ClientException;
- 发送事务消息
SendReceipt send(Message message, Transaction transaction) throws ClientException;
- 异步发送
CompletableFuture<SendReceipt> sendAsync(Message message);
- 开始事务
Transaction beginTransaction() throws ClientException;
- 关闭和释放生产者资源
void close() throws IOException;
来看看发送消息是怎么执行的
/**
* @see Producer#send(Message)
*/
@Override
public SendReceipt send(Message message) throws ClientException {
//注册ListenableFuture,input 是发送消息 Function是拿到消息id 执行器是 DirectExecutor
final ListenableFuture<SendReceipt> future = Futures.transform(send(Collections.singletonList(message), false),
sendReceipts -> sendReceipts.iterator().next(), MoreExecutors.directExecutor());
return handleClientFuture(future);
}
来看看具体执行的send方法
private ListenableFuture<List<SendReceiptImpl>> send(List<Message> messages, boolean txEnabled) {
SettableFuture<List<SendReceiptImpl>> future = SettableFuture.create();
// 在消息发布前检查生产者状态。
if (!this.isRunning()) {
final IllegalStateException e = new IllegalStateException("Producer is not running now");
future.setException(e);
log.error("Unable to send message because producer is not running, state={}, clientId={}",
this.state(), clientId);
return future;
}
//构建 发布消息 列表,这里可以在ArrayList<>()加上messages.size()减少扩容带来的性能问题
List<PublishingMessageImpl> pubMessages = new ArrayList<>();
for (Message message : messages) {
try {
//生成消息,主要的工作是 生成消息id和确认消息类型
final PublishingMessageImpl pubMessage = new PublishingMessageImpl(message, publishingSettings,
txEnabled);
pubMessages.add(pubMessage);
} catch (Throwable t) {
// Failed to refine message, no need to proceed.
log.error("Failed to refine message to send, clientId={}, message={}", clientId, message, t);
future.setException(t);
return future;
}
}
// 整理消息主题成 set
final Set<String> topics = pubMessages.stream().map(Message::getTopic).collect(Collectors.toSet());
if (1 < topics.size()) {
// Messages have different topics, no need to proceed.
final IllegalArgumentException e = new IllegalArgumentException("Messages to send have different topics");
future.setException(e);
log.error("Messages to be sent have different topics, no need to proceed, topic(s)={}, clientId={}",
topics, clientId);
return future;
}
final String topic = topics.iterator().next();
// 整理消息类型成 set
final Set<MessageType> messageTypes = pubMessages.stream()
.map(PublishingMessageImpl::getMessageType)
.collect(Collectors.toSet());
if (1 < messageTypes.size()) {
// Messages have different message type, no need to proceed.
final IllegalArgumentException e = new IllegalArgumentException("Messages to send have different types, "
+ "please check");
future.setException(e);
log.error("Messages to be sent have different message types, no need to proceed, topic={}, messageType"
+ "(s)={}, clientId={}", topic, messageTypes, clientId, e);
return future;
}
final MessageType messageType = messageTypes.iterator().next();
final String messageGroup;
// 如果消息类型为FIFO,则整理消息组程 set
if (MessageType.FIFO.equals(messageType)) {
final Set<String> messageGroups = pubMessages.stream()
.map(PublishingMessageImpl::getMessageGroup).filter(Optional::isPresent)
.map(Optional::get).collect(Collectors.toSet());
if (1 < messageGroups.size()) {
final IllegalArgumentException e = new IllegalArgumentException("FIFO messages to send have different "
+ "message groups, messageGroups=" + messageGroups);
future.setException(e);
log.error("FIFO messages to be sent have different message groups, no need to proceed, topic={}, "
+ "messageGroups={}, clientId={}", topic, messageGroups, clientId, e);
return future;
}
messageGroup = messageGroups.iterator().next();
} else {
messageGroup = null;
}
this.topics.add(topic);
// 获取主题路由
final ListenableFuture<PublishingLoadBalancer> routeFuture = getPublishingLoadBalancer(topic);
return Futures.transformAsync(routeFuture, result -> {
// 预备重试队列
final List<MessageQueueImpl> candidates = null == messageGroup ? takeMessageQueues(result) :
Collections.singletonList(result.takeMessageQueueByMessageGroup(messageGroup));
final SettableFuture<List<SendReceiptImpl>> future0 = SettableFuture.create();
send0(future0, topic, messageType, candidates, pubMessages, 1);
return future0;
}, MoreExecutors.directExecutor());
}
走到这里只是处理好了消息和发送路由,来看看具体的发送#send0
/**
* Warning: please DO NOT modify the signature of this method, it is used by OpenTelemetry instrumentation.
*/
private void send0(SettableFuture<List<SendReceiptImpl>> future0, String topic, MessageType messageType,
final List<MessageQueueImpl> candidates, final List<PublishingMessageImpl> messages, final int attempt) {
// 找到当前消息的消息队列
final MessageQueueImpl mq = candidates.get(IntMath.mod(attempt - 1, candidates.size()));
final List<MessageType> acceptMessageTypes = mq.getAcceptMessageTypes();
if (publishingSettings.isValidateMessageType() && !acceptMessageTypes.contains(messageType)) {
final IllegalArgumentException e = new IllegalArgumentException("Current message type not match with "
+ "topic accept message types, topic=" + topic + ", actualMessageType=" + messageType + ", "
+ "acceptMessageTypes=" + acceptMessageTypes);
future0.setException(e);
return;
}
//找到接入点
final Endpoints endpoints = mq.getBroker().getEndpoints();
//发送消息
final ListenableFuture<List<SendReceiptImpl>> future = send0(endpoints, messages, mq);
//获取最大重试次数
final int maxAttempts = this.getRetryPolicy().getMaxAttempts();
// 执行消息前置拦截器
final List<GeneralMessage> generalMessages = messages.stream().map((Function<PublishingMessageImpl,
GeneralMessage>) GeneralMessageImpl::new).collect(Collectors.toList());
final MessageInterceptorContextImpl context = new MessageInterceptorContextImpl(MessageHookPoints.SEND);
doBefore(context, generalMessages);
//为future添加回调
Futures.addCallback(future, new FutureCallback<List<SendReceiptImpl>>() {
@Override
public void onSuccess(List<SendReceiptImpl> sendReceipts) {
// Should never reach here. 如果回执list和消息list长度不一致
if (sendReceipts.size() != messages.size()) {
final InternalErrorException e = new InternalErrorException("[Bug] due to an"
+ " unknown reason from remote, received send receipt's quantity " + sendReceipts.size()
+ " is not equal to sent message's quantity " + messages.size());
future0.setException(e);
// Intercept after message publishing.
final MessageInterceptorContextImpl context0 = new MessageInterceptorContextImpl(context,
MessageHookPointsStatus.ERROR);
doAfter(context0, generalMessages);
return;
}
// 处理消息后置拦截器
final MessageInterceptorContextImpl context0 = new MessageInterceptorContextImpl(context,
MessageHookPointsStatus.OK);
doAfter(context0, generalMessages);
// 不需要重试
future0.set(sendReceipts);
// Resend message(s) successfully.
if (1 < attempt) {
// Collect messageId(s) for logging.
List<MessageId> messageIds = new ArrayList<>();
for (SendReceipt receipt : sendReceipts) {
messageIds.add(receipt.getMessageId());
}
log.info("Resend message successfully, topic={}, messageId(s)={}, maxAttempts={}, "
+ "attempt={}, endpoints={}, clientId={}", topic, messageIds, maxAttempts, attempt,
endpoints, clientId);
}
//该消息一次就成功了
}
//处理Throwable
@Override
public void onFailure(Throwable t) {
// 处理消息后置拦截器
final MessageInterceptorContextImpl context0 = new MessageInterceptorContextImpl(context,
MessageHookPointsStatus.ERROR);
doAfter(context0, generalMessages);
// 手机消息id
List<MessageId> messageIds = new ArrayList<>();
for (PublishingMessageImpl message : messages) {
messageIds.add(message.getMessageId());
}
// 隔离broker接入点 !!! broker路由时有参与计算
isolate(endpoints);
if (attempt >= maxAttempts) {
// No need more attempts.
future0.setException(t);
log.error("Failed to send message(s) finally, run out of attempt times, maxAttempts={}, " +
"attempt={}, topic={}, messageId(s)={}, endpoints={}, clientId={}",
maxAttempts, attempt, topic, messageIds, endpoints, clientId, t);
return;
}
// 如果是事务消息,则不再尝试
if (MessageType.TRANSACTION.equals(messageType)) {
future0.setException(t);
log.error("Failed to send transactional message finally, maxAttempts=1, attempt={}, " +
"topic={}, messageId(s)={}, endpoints={}, clientId={}", attempt, topic, messageIds,
endpoints, clientId, t);
return;
}
// 递增重试次数
int nextAttempt = 1 + attempt;
// 达到上限直接借宿
if (!(t instanceof TooManyRequestsException)) {
log.warn("Failed to send message, would attempt to resend right now, maxAttempts={}, "
+ "attempt={}, topic={}, messageId(s)={}, endpoints={}, clientId={}", maxAttempts, attempt,
topic, messageIds, endpoints, clientId, t);
send0(future0, topic, messageType, candidates, messages, nextAttempt);
return;
}
final Duration delay = ProducerImpl.this.getRetryPolicy().getNextAttemptDelay(nextAttempt);
log.warn("Failed to send message due to too many requests, would attempt to resend after {}, "
+ "maxAttempts={}, attempt={}, topic={}, messageId(s)={}, endpoints={}, clientId={}", delay,
maxAttempts, attempt, topic, messageIds, endpoints, clientId, t);
ProducerImpl.this.getClientManager().getScheduler().schedule(() -> send0(future0, topic, messageType,
candidates, messages, nextAttempt), delay.toNanos(), TimeUnit.NANOSECONDS);
}
}, clientCallbackExecutor);
}
private void send0(SettableFuture<List<SendReceiptImpl>>,String,MessageType,List<MessageQueueImpl>,List<PublishingMessageImpl>,int)
方法中还有一个 ListenableFuture<List<SendReceiptImpl>> send0(Endpoints,List<PublishingMessageImpl>,MessageQueueImpl)
方法,这里是具体将请求发送到消息队列的方法
ListenableFuture<List<SendReceiptImpl>> send0(Endpoints endpoints, List<PublishingMessageImpl> pubMessages,
MessageQueueImpl mq) {
//构建请求
final SendMessageRequest request = wrapSendMessageRequest(pubMessages, mq);
//构建RpcFuture 具体的是 调用client的sendMessage方法
final RpcFuture<SendMessageRequest, SendMessageResponse> future0 =
this.getClientManager().sendMessage(endpoints, request, clientConfiguration.getRequestTimeout());
//使用DirectExecutor执行future并且处理response成SendReceiptImpl
return Futures.transformAsync(future0,
response -> Futures.immediateFuture(SendReceiptImpl.processResponseInvocation(mq, response, future0)),
MoreExecutors.directExecutor());
}
来看看具体的ClientManager#sendMessage()
,这里具体的就是构建请求信息,通过grpc写到管道中
@Override
public RpcFuture<SendMessageRequest, SendMessageResponse> sendMessage(Endpoints endpoints,
SendMessageRequest request, Duration duration) {
try {
final Metadata metadata = client.sign();
final Context context = new Context(endpoints, metadata);
final RpcClient rpcClient = getRpcClient(endpoints);
final ListenableFuture<SendMessageResponse> future =
rpcClient.sendMessage(metadata, request, asyncWorker, duration);
return new RpcFuture<>(context, request, future);
} catch (Throwable t) {
return new RpcFuture<>(t);
}
}
总结
整体发送过程就是
- 通过配置信息构建生产者
- 构建要发送的消息
- 通过和broker的grpc通道发送消息
extent
这里有一点需要注意,网上的八股文都说发送时,数据超过4k会进行压缩,但rocket-clients项目下的java包中的Producer并无此代码体现,在RocketMQ项目中rocketmq-client的DefaultMQProducer
实现类中有体现,具体的为判断是否超过设定的参数compressMsgBodyOverHowmuch
,如果超过了该参数会根据配置的rocketmq.message.compressType
获取压缩器,并对消息进行压缩,压缩器可选ZLIB
,ZSTD
,LZ4
,这个包对应的maven位置应该是
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>${rocketmq.verison}</version>
</dependency>
源码如下:DefaultMQProducerImpl#sendKernelImpl
// 压缩层级
private int compressLevel = Integer.parseInt(System.getProperty(MixAll.MESSAGE_COMPRESS_LEVEL, "5"));
//压缩器
private CompressionType compressType = CompressionType.of(System.getProperty(MixAll.MESSAGE_COMPRESS_TYPE, "ZLIB"));
private final Compressor compressor = CompressorFactory.getCompressor(compressType);
if (this.tryToCompressMessage(msg)) {
sysFlag |= MessageSysFlag.COMPRESSED_FLAG;
sysFlag |= compressType.getCompressionFlag();
msgBodyCompressed = true;
}
private boolean tryToCompressMessage(final Message msg) {
if (msg instanceof MessageBatch) {
//batch does not support compressing right now
return false;
}
byte[] body = msg.getBody();
if (body != null) {
if (body.length >= this.defaultMQProducer.getCompressMsgBodyOverHowmuch()) {
try {
byte[] data = compressor.compress(body, compressLevel);
if (data != null) {
msg.setBody(data);
return true;
}
} catch (IOException e) {
log.error("tryToCompressMessage exception", e);
if (log.isDebugEnabled()) {
log.debug(msg.toString());
}
}
}
}
return false;
}
消费消息
官网给我们提供了这样的消费示例
import java.io.IOException;
import java.util.Collections;
import org.apache.rocketmq.client.apis.ClientConfiguration;
import org.apache.rocketmq.client.apis.ClientException;
import org.apache.rocketmq.client.apis.ClientServiceProvider;
import org.apache.rocketmq.client.apis.consumer.ConsumeResult;
import org.apache.rocketmq.client.apis.consumer.FilterExpression;
import org.apache.rocketmq.client.apis.consumer.FilterExpressionType;
import org.apache.rocketmq.client.apis.consumer.PushConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ConsumerExample {
private static final Logger logger = LoggerFactory.getLogger(PushConsumerExample.class);
private ConsumerExample() {
}
public static void main(String[] args) throws ClientException, IOException, InterruptedException {
final ClientServiceProvider provider = ClientServiceProvider.loadService();
// 接入点地址,需要设置成Proxy的地址和端口列表,一般是xxx:8081;xxx:8081。
String endpoints = "localhost:8081";
ClientConfiguration clientConfiguration = ClientConfiguration.newBuilder()
.setEndpoints(endpoints)
.build();
// 订阅消息的过滤规则,表示订阅所有Tag的消息。
String tag = "*";
FilterExpression filterExpression = new FilterExpression(tag, FilterExpressionType.TAG);
// 为消费者指定所属的消费者分组,Group需要提前创建。
String consumerGroup = "YourConsumerGroup";
// 指定需要订阅哪个目标Topic,Topic需要提前创建。
String topic = "TestTopic";
// 初始化PushConsumer,需要绑定消费者分组ConsumerGroup、通信参数以及订阅关系。
PushConsumer pushConsumer = provider.newPushConsumerBuilder()
.setClientConfiguration(clientConfiguration)
// 设置消费者分组。
.setConsumerGroup(consumerGroup)
// 设置预绑定的订阅关系。
.setSubscriptionExpressions(Collections.singletonMap(topic, filterExpression))
// 设置消费监听器。
.setMessageListener(messageView -> {
// 处理消息并返回消费结果。
logger.info("Consume message successfully, messageId={}", messageView.getMessageId());
return ConsumeResult.SUCCESS;
})
.build();
Thread.sleep(Long.MAX_VALUE);
// 如果不需要再使用 PushConsumer,可关闭该实例。
// pushConsumer.close();
}
}
构建配置信息的过程和生产者类似,这里略过
构建消费者实例
/**
* @see PushConsumerBuilder#build()
*/
@Override
public PushConsumer build() throws ClientException {
checkNotNull(clientConfiguration, "clientConfiguration has not been set yet");
checkNotNull(consumerGroup, "consumerGroup has not been set yet");
checkNotNull(messageListener, "messageListener has not been set yet");
checkArgument(!subscriptionExpressions.isEmpty(), "subscriptionExpressions have not been set yet");
final PushConsumerImpl pushConsumer = new PushConsumerImpl(clientConfiguration, consumerGroup,
subscriptionExpressions, messageListener, maxCacheMessageCount, maxCacheMessageSizeInBytes,
consumptionThreadCount);
pushConsumer.startAsync().awaitRunning();
return pushConsumer;
}
PushConsumerImpl
继承了ConsumerImpl
并且实现了PushConsumer
class PushConsumerImpl extends ConsumerImpl implements PushConsumer {
来看看PushConsumerImpl的实例化方法
public PushConsumerImpl(ClientConfiguration clientConfiguration, String consumerGroup,
Map<String, FilterExpression> subscriptionExpressions, MessageListener messageListener,
int maxCacheMessageCount, int maxCacheMessageSizeInBytes, int consumptionThreadCount) {
//构建ConsumerImpl
super(clientConfiguration, consumerGroup, subscriptionExpressions.keySet());
this.clientConfiguration = clientConfiguration;
Resource groupResource = new Resource(clientConfiguration.getNamespace(), consumerGroup);
//发布配置
this.pushSubscriptionSettings = new PushSubscriptionSettings(clientConfiguration.getNamespace(), clientId,
endpoints, groupResource, clientConfiguration.getRequestTimeout(), subscriptionExpressions);
//所属消费者组
this.consumerGroup = consumerGroup;
//订阅配置
this.subscriptionExpressions = subscriptionExpressions;
this.cacheAssignments = new ConcurrentHashMap<>();
//消息监听器
this.messageListener = messageListener;
//最大缓存的消息数和消息大小
this.maxCacheMessageCount = maxCacheMessageCount;
this.maxCacheMessageSizeInBytes = maxCacheMessageSizeInBytes;
//消息接收次数
this.receptionTimes = new AtomicLong(0);
//消息接收数量
this.receivedMessagesQuantity = new AtomicLong(0);
// 消费者的 ok error 计数器
this.consumptionOkQuantity = new AtomicLong(0);
this.consumptionErrorQuantity = new AtomicLong(0);
//队列表
this.processQueueTable = new ConcurrentHashMap<>();
// 消费者线程池
this.consumptionExecutor = new ThreadPoolExecutor(
consumptionThreadCount,
consumptionThreadCount,
60,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
new ThreadFactoryImpl("MessageConsumption", this.getClientId().getIndex()));
}
实例化方法配置了一堆的信息后,我们来看看PushConsumerImpl
的#startUp()
方法
@Override
protected void startUp() throws Exception {
try {
log.info("Begin to start the rocketmq push consumer, clientId={}", clientId);
GaugeObserver gaugeObserver = new ProcessQueueGaugeObserver(processQueueTable, clientId, consumerGroup);
this.clientMeterManager.setGaugeObserver(gaugeObserver);
//调用ClientImpl的startUp方法,该方法会去刷新topic路由
super.startUp();
final ScheduledExecutorService scheduler = this.getClientManager().getScheduler();
//创建消费服务
this.consumeService = createConsumeService();
// 延迟1秒,每隔5秒去scanAssignments一次(队列信息,在4.X版本是在rebalance时调用,具体的是通过rpc调用服务)
//具体的就是去拉 接入点的队列,并进行处理 目的是同步队列信息
scanAssignmentsFuture = scheduler.scheduleWithFixedDelay(() -> {
try {
scanAssignments();
} catch (Throwable t) {
log.error("Exception raised while scanning the load assignments, clientId={}", clientId, t);
}
}, 1, 5, TimeUnit.SECONDS);
log.info("The rocketmq push consumer starts successfully, clientId={}", clientId);
} catch (Throwable t) {
log.error("Exception raised while starting the rocketmq push consumer, clientId={}", clientId, t);
shutDown();
throw t;
}
}
消费者最为重要的是怎么去消费,我们再来看看#createConsumeService
方法做了什么
private ConsumeService createConsumeService() {
final ScheduledExecutorService scheduler = this.getClientManager().getScheduler();
if (pushSubscriptionSettings.isFifo()) {
log.info("Create FIFO consume service, consumerGroup={}, clientId={}", consumerGroup, clientId);
return new FifoConsumeService(clientId, messageListener, consumptionExecutor, this, scheduler);
}
log.info("Create standard consume service, consumerGroup={}, clientId={}", consumerGroup, clientId);
return new StandardConsumeService(clientId, messageListener, consumptionExecutor, this, scheduler);
}
#createConsumeService
方法根据队列了行去构建消费服务,我们按照普通消息来看
public class StandardConsumeService extends ConsumeService {
private static final Logger log = LoggerFactory.getLogger(StandardConsumeService.class);
public StandardConsumeService(ClientId clientId, MessageListener messageListener,
ThreadPoolExecutor consumptionExecutor, MessageInterceptor messageInterceptor,
ScheduledExecutorService scheduler) {
super(clientId, messageListener, consumptionExecutor, messageInterceptor, scheduler);
}
@Override
public void consume(ProcessQueue pq, List<MessageViewImpl> messageViews) {
for (MessageViewImpl messageView : messageViews) {
// Discard corrupted message.
if (messageView.isCorrupted()) {
log.error("Message is corrupted for standard consumption, prepare to discard it, mq={}, "
+ "messageId={}, clientId={}", pq.getMessageQueue(), messageView.getMessageId(), clientId);
pq.discardMessage(messageView);
continue;
}
//调用父类ConsumeService的consume(MessageViewImpl)方法
final ListenableFuture<ConsumeResult> future = consume(messageView);
Futures.addCallback(future, new FutureCallback<ConsumeResult>() {
@Override
public void onSuccess(ConsumeResult consumeResult) {
//处理消息ack和本地消息缓存
pq.eraseMessage(messageView, consumeResult);
}
@Override
public void onFailure(Throwable t) {
// Should never reach here.
log.error("[Bug] Exception raised in consumption callback, clientId={}", clientId, t);
}
}, MoreExecutors.directExecutor());
}
}
}
看到这里,好像就构建了个消费者对象,也没见哪里开始获取消息呀? 我也很好奇,接着来看看
拉取消息
@VisibleForTesting
void scanAssignments() {
try {
log.debug("Start to scan assignments periodically, clientId={}", clientId);
for (Map.Entry<String, FilterExpression> entry : subscriptionExpressions.entrySet()) {
final String topic = entry.getKey();
final FilterExpression filterExpression = entry.getValue();
final Assignments existed = cacheAssignments.get(topic);
final ListenableFuture<Assignments> future = queryAssignment(topic);
Futures.addCallback(future, new FutureCallback<Assignments>() {
@Override
public void onSuccess(Assignments latest) {
if (latest.getAssignmentList().isEmpty()) {
if (null == existed || existed.getAssignmentList().isEmpty()) {
log.info("Acquired empty assignments from remote, would scan later, topic={}, "
+ "clientId={}", topic, clientId);
return;
}
log.info("Attention!!! acquired empty assignments from remote, but existed assignments"
+ " is not empty, topic={}, clientId={}", topic, clientId);
}
if (!latest.equals(existed)) {
log.info("Assignments of topic={} has changed, {} => {}, clientId={}", topic, existed,
latest, clientId);
//在这里 如果队列数据不对等,会去处理队列和更新缓存的数据
syncProcessQueue(topic, latest, filterExpression);
cacheAssignments.put(topic, latest);
return;
}
log.debug("Assignments of topic={} remains the same, assignments={}, clientId={}", topic,
existed, clientId);
// Process queue may be dropped, need to be synchronized anyway.
syncProcessQueue(topic, latest, filterExpression);
}
@Override
public void onFailure(Throwable t) {
log.error("Exception raised while scanning the assignments, topic={}, clientId={}", topic,
clientId, t);
}
}, MoreExecutors.directExecutor());
}
} catch (Throwable t) {
log.error("Exception raised while scanning the assignments for all topics, clientId={}", clientId, t);
}
}
队列出现更新的时候,会去调用#syncProcessQueue
方法
void syncProcessQueue(String topic, Assignments assignments, FilterExpression filterExpression) {
Set<MessageQueueImpl> latest = new HashSet<>();
final List<Assignment> assignmentList = assignments.getAssignmentList();
for (Assignment assignment : assignmentList) {
latest.add(assignment.getMessageQueue());
}
Set<MessageQueueImpl> activeMqs = new HashSet<>();
//判断队列表中的队列是否健康
for (Map.Entry<MessageQueueImpl, ProcessQueue> entry : processQueueTable.entrySet()) {
final MessageQueueImpl mq = entry.getKey();
final ProcessQueue pq = entry.getValue();
if (!topic.equals(mq.getTopic())) {
continue;
}
if (!latest.contains(mq)) {
log.info("Drop message queue according to the latest assignmentList, mq={}, clientId={}", mq,
clientId);
dropProcessQueue(mq);
continue;
}
if (pq.expired()) {
log.warn("Drop message queue because it is expired, mq={}, clientId={}", mq, clientId);
dropProcessQueue(mq);
continue;
}
activeMqs.add(mq);
}
//把不在本地队列表中的队列加入到对列表中,并调用ProcessQueue的#fetchMessageImmediately
for (MessageQueueImpl mq : latest) {
if (activeMqs.contains(mq)) {
continue;
}
final Optional<ProcessQueue> optionalProcessQueue = createProcessQueue(mq, filterExpression);
if (optionalProcessQueue.isPresent()) {
log.info("Start to fetch message from remote, mq={}, clientId={}", mq, clientId);
optionalProcessQueue.get().fetchMessageImmediately();
}
}
}
然后我们来看ProcessQueue的#fetchMessageImmediately方法
public interface ProcessQueue {
/**
* Start to fetch messages from remote immediately.
* 立即开始从远程获取消息。
*/
void fetchMessageImmediately();
就是这一步,开始从远程服务器拉取消息进行消费,来看看具体是怎么做的
ProcessQueueImpl#fetchMessageImmediately
@Override
public void fetchMessageImmediately() {
receiveMessageImmediately();
}
private String generateAttemptId() {
return UUID.randomUUID().toString();
}
private void receiveMessageImmediately() {
receiveMessageImmediately(this.generateAttemptId());
}
private void receiveMessageImmediately(String attemptId) {
final ClientId clientId = consumer.getClientId();
//判断消费者是否活着
if (!consumer.isRunning()) {
log.info("Stop to receive message because consumer is not running, mq={}, clientId={}", mq, clientId);
return;
}
try {
//获取接入点信息
final Endpoints endpoints = mq.getBroker().getEndpoints();
//计算一批最多多少, 最大缓存数/队列数-已缓存的消息数 最小值为1
final int batchSize = this.getReceptionBatchSize();
//长轮训的超时时间
final Duration longPollingTimeout = consumer.getPushConsumerSettings().getLongPollingTimeout();
//构建接受消息的请求
final ReceiveMessageRequest request = consumer.wrapReceiveMessageRequest(batchSize, mq, filterExpression,
longPollingTimeout, attemptId);
activityNanoTime = System.nanoTime();
// 执行消息拦截器前置doBefore
final MessageInterceptorContextImpl context = new MessageInterceptorContextImpl(MessageHookPoints.RECEIVE);
consumer.doBefore(context, Collections.emptyList());
//构建接受消息future
final ListenableFuture<ReceiveMessageResult> future = consumer.receiveMessage(request, mq,
longPollingTimeout);
//注册回调
Futures.addCallback(future, new FutureCallback<ReceiveMessageResult>() {
@Override
public void onSuccess(ReceiveMessageResult result) {
// 执行消息后置拦截器 doAfter
final List<GeneralMessage> generalMessages = result.getMessageViewImpls().stream()
.map((Function<MessageView, GeneralMessage>) GeneralMessageImpl::new)
.collect(Collectors.toList());
final MessageInterceptorContextImpl context0 =
new MessageInterceptorContextImpl(context, MessageHookPointsStatus.OK);
consumer.doAfter(context0, generalMessages);
//处理消息
try {
onReceiveMessageResult(result);
} catch (Throwable t) {
// Should never reach here.
log.error("[Bug] Exception raised while handling receive result, mq={}, endpoints={}, "
+ "clientId={}", mq, endpoints, clientId, t);
onReceiveMessageException(t, attemptId);
}
}
@Override
public void onFailure(Throwable t) {
String nextAttemptId = null;
if (t instanceof StatusRuntimeException) {
StatusRuntimeException exception = (StatusRuntimeException) t;
if (io.grpc.Status.DEADLINE_EXCEEDED.getCode() == exception.getStatus().getCode()) {
nextAttemptId = request.getAttemptId();
}
}
// 执行消息后置拦截器 doAfter
final MessageInterceptorContextImpl context0 =
new MessageInterceptorContextImpl(context, MessageHookPointsStatus.ERROR);
consumer.doAfter(context0, Collections.emptyList());
log.error("Exception raised during message reception, mq={}, endpoints={}, attemptId={}, " +
"nextAttemptId={}, clientId={}", mq, endpoints, request.getAttemptId(), nextAttemptId,
clientId, t);
//处理接受异常,具体的操作是 延迟再接收
onReceiveMessageException(t, nextAttemptId);
}
}, MoreExecutors.directExecutor());
receptionTimes.getAndIncrement();
consumer.getReceptionTimes().getAndIncrement();
} catch (Throwable t) {
log.error("Exception raised during message reception, mq={}, clientId={}", mq, clientId, t);
onReceiveMessageException(t, attemptId);
}
}
我们来看看正常消费消息是做了什么,具体的方法是#onReceiveMessageResult
private void onReceiveMessageResult(ReceiveMessageResult result) {
final List<MessageViewImpl> messages = result.getMessageViewImpls();
if (!messages.isEmpty()) {
//缓存消息并计算已经缓存的消息大小
cacheMessages(messages);
//更新接收到的消息计数器
receivedMessagesQuantity.getAndAdd(messages.size());
//更新消费者已经收到的消息计数器
consumer.getReceivedMessagesQuantity().getAndAdd(messages.size());
//进行消费
consumer.getConsumeService().consume(this, messages);
}
//再次接受消息
receiveMessage();
}
消息处理
从前面的StandardConsumeService
可知,具体的消费是调用父类ConsumeService的consume(MessageViewImpl)方法
public ListenableFuture<ConsumeResult> consume(MessageViewImpl messageView) {
return consume(messageView, Duration.ZERO);
}
public ListenableFuture<ConsumeResult> consume(MessageViewImpl messageView, Duration delay) {
final ListeningExecutorService executorService = MoreExecutors.listeningDecorator(consumptionExecutor);
final ConsumeTask task = new ConsumeTask(clientId, messageListener, messageView, messageInterceptor);
// 立刻消费
if (Duration.ZERO.compareTo(delay) >= 0) {
//立刻提交给消费者线程池处理消息
return executorService.submit(task);
}
//延迟消费
final SettableFuture<ConsumeResult> future0 = SettableFuture.create();
scheduler.schedule(() -> {
final ListenableFuture<ConsumeResult> future = executorService.submit(task);
Futures.addCallback(future, new FutureCallback<ConsumeResult>() {
@Override
public void onSuccess(ConsumeResult consumeResult) {
future0.set(consumeResult);
}
@Override
public void onFailure(Throwable t) {
// Should never reach here.
log.error("[Bug] Exception raised while submitting scheduled consumption task, clientId={}",
clientId, t);
}
}, MoreExecutors.directExecutor());
}, delay.toNanos(), TimeUnit.NANOSECONDS);
return future0;
}
具体的是 构建ConsumeTask
后提交给消费者线程池进行处理,来看看ConsumeTask#call()
@Override
public ConsumeResult call() {
ConsumeResult consumeResult;
//处理消息处理前置拦截器
final List<GeneralMessage> generalMessages = Collections.singletonList(new GeneralMessageImpl(messageView));
MessageInterceptorContextImpl context = new MessageInterceptorContextImpl(MessageHookPoints.CONSUME);
messageInterceptor.doBefore(context, generalMessages);
//调用创建的Listen进行消息消费
try {
consumeResult = messageListener.consume(messageView);
} catch (Throwable t) {
log.error("Message listener raised an exception while consuming messages, clientId={}, mq={}, " +
"messageId={}", clientId, messageView.getMessageQueue(), messageView.getMessageId(), t);
// If exception was thrown during the period of message consumption, mark it as failure.
consumeResult = ConsumeResult.FAILURE;
}
//计算消费结果
MessageHookPointsStatus status = ConsumeResult.SUCCESS.equals(consumeResult) ? MessageHookPointsStatus.OK :
MessageHookPointsStatus.ERROR;
context = new MessageInterceptorContextImpl(context, status);
//后置拦截器
messageInterceptor.doAfter(context, generalMessages);
// Make sure that the return value is the subset of messageViews.
return consumeResult;
}
总结
rocket消费者消费消息的整个流程:
- 构建消费者client
- 获取topic路由,并定时刷新topic路由
- 根据topic获取队列信息,并定时更新队列信息
- 通过rpc获取队列消息
- 消费消息