RocketMQ路由中心NameServer
重点内容如下。
● NameServer整体架构设计
● NameServer动态路由发现与剔除机制
NameServer架构设计
消息中间件的设计思路一般基于主题的订阅发布机制,消息生产者(Producer)发送某一主题的消息到消息服务器,消息服务器负责该消息的持久化存储,消息消费者(Consumer)订阅感兴趣的主题,消息服务器根据订阅信息(路由信息)将消息推送到消费者(PUSH模式)或者消息消费者主动向消息服务器拉取消息(PULL模式),从而实现消息生产者与消息消费者解耦。为了避免消息服务器的单点故障导致的整个系统瘫痪,通常会部署多台消息服务器共同承担消息的存储。那消息生产者如何知道消息要发往哪台消息服务器呢?如果某一台消息服务器宕机了,那么生产者如何在不重启服务的情况下感知呢?NameServer就是为了解决上述问题而设计的。
Broker消息服务器在启动时向所有NameServer注册,消息生产者(Producer)在发送消息之前先从NameServer获取Broker服务器地址列表,然后根据负载算法从列表中选择一台消息服务器进行消息发送。NameServer与每台Broker服务器保持长连接,并间隔30s检测Broker是否存活,如果检测到Broker宕机,则从路由注册表中将其移除。但是路由变化不会马上通知消息生产者,为什么要这样设计呢?这是为了降低NameServer实现的复杂性,在消息发送端提供容错机制来保证消息发送的高可用性
NameServer路由注册、故障剔除
NameServer主要作用是为消息生产者和消息消费者提供关于主题Topic的路由信息,那么NameServer需要存储路由的基础信息,还要能够管理Broker节点,包括路由注册、路由删除等功能。
□ topicQueueTable:Topic消息队列路由信息,消息发送时根据路由表进行负载均衡。□ brokerAddrTable:Broker基础信息,包含brokerName、所属集群名称、主备Broker地址。□ clusterAddrTable:Broker集群信息,存储集群中所有Broker名称。□ brokerLiveTable:Broker状态信息。NameServer每次收到心跳包时会替换该信息。□ filterServerTable:Broker上的FilterServer列表,用于类模式消息过滤。
RocketMQ基于订阅发布机制,一个Topic拥有多个消息队列,一个Broker为每一主题默认创建4个读队列4个写队列。多个Broker组成一个集群,BrokerName由相同的多台Broker组成Master-Slave架构,brokerId为0代表Master,大于0表示Slave。BrokerLiveInfo中的lastUpdateTimestamp存储上次收到Broker心跳包的时间。
运行时内存结构:
路由注册RocketMQ路由注册是通过Broker与NameServer的心跳功能实现的。
Broker启动时向集群中所有的NameServer发送心跳语句,每隔30s向集群中所有NameServer发送心跳包,NameServer收到Broker心跳包时会更新brokerLiveTable缓存中BrokerLiveInfo的lastUpdate Timestamp,然后Name Server每隔10s扫描brokerLiveTable,如果连续120s没有收到心跳包,NameServer将移除该Broker的路由信息同时关闭Socket连接。
路由注册需要加写锁,防止并发修改RouteInfoManager中的路由表。首先判断Broker所属集群是否存在,如果不存在,则创建,然后将broker名加入到集群Broker集合中。
设计亮点:NameServe与Broker保持长连接,Broker状态存储在brokerLiveTable中,NameServer每收到一个心跳包,将更新brokerLiveTable中关于Broker的状态信息以及路由表(topicQueueTable、brokerAddrTable、brokerLiveTable、filterServerTable)。更新上述路由表(HashTable)使用了锁粒度较少的读写锁,允许多个消息发送者(Producer)并发读,保证消息发送时的高并发。但同一时刻NameServer只处理一个Broker心跳包,多个心跳包请求串行执行。这也是读写锁经典使用场景,更多关于读写锁的信息,可以参考笔者的博文:http://blog.csdn.net/prestigeding/article/details/53286756。
Broker每隔30s向NameServer发送一个心跳包,心跳包中包含BrokerId、Broker地址、Broker名称、Broker所属集群名称、Broker关联的FilterServer列表。但是如果Broker宕机,NameServer无法收到心跳包,此时NameServer如何来剔除这些失效的Broker呢?Name Server会每隔10s扫描brokerLiveTable状态表,如果BrokerLive的lastUpdateTimestamp的时间戳距当前时间超过120s,则认为Broker失效,移除该Broker,关闭与Broker连接,并同时更新topicQueueTable、brokerAddrTable、brokerLiveTable、filterServerTable。
RocketMQ路由发现是非实时的,当Topic路由出现变化后,NameServer不主动推送给客户端,而是由客户端定时拉取主题最新的路由。根据主题名称拉取路由信息的命令编码为:GET_ROUTEINTO_BY_TOPIC
总结:
路由注册删除机制:
疑问:NameServer需要等Broker失效至少120s才能将该Broker从路由表中移除掉,那如果在Broker故障期间,消息生产者Producer根据主题获取到的路由信息包含已经宕机的Broker,会导致消息发送失败,那这种情况怎么办,岂不是消息发送不是高可用的?
*RocketMQ消息发送
RocketMQ发送普通消息有三种实现方式:可靠同步发送、可靠异步发送、单向(Oneway)发送。第3章主要聚焦在RocketMQ如何发送消息,然后从消息的数据结构开始,逐步介绍生产者的启动流程和消息发送的流程,最后再详细阐述批量消息发送。
● RocketMQ消息结构
● 消息生产者(Producer)启动流程
● 消息发送过程
● 批量消息发送
同步:发送者向MQ执行发送消息API时,同步等待,直到消息服务器返回发送结果。
异步:发送者向MQ执行发送消息API时,指定消息发送成功后的回掉函数,然后调用消息发送API后,立即返回,消息发送者线程不阻塞,直到运行结束,消息发送成功或失败的回调任务在一个新的线程中执行。
单向:消息发送者向MQ执行发送消息API时,直接返回,不等待消息服务器的结果,也不注册回调函数,简单地说,就是只管发,不在乎消息是否成功存储在消息服务器上。
RocketMQ消息发送需要考虑以下几个问题:
□ 消息队列如何进行负载?
□ 消息发送如何实现高可用?
□ 批量消息发送如何实现一致性?
消息发送之前,首先需要获取主题的路由信息,只有获取了这些信息我们才知道消息要发送到具体的Broker节点。
如果生产者中缓存了topic的路由信息,如果该路由信息中包含了消息队列,则直接返回该路由信息,如果没有缓存或没有包含消息队列,则向NameServer查询该topic的路由信息。如果最终未找到路由信息,则抛出异常:无法找到主题相关路由信息异常
如果isDefault为true,则使用默认主题去查询,如果查询到路由信息,则替换路由信息中读写队列个数为消息生产者默认的队列个数(defaultTopicQueueNums);如果isDefault为false,则使用参数topic去查询;如果未查询到路由信息,则返回false,表示路由信息未变化
如果路由信息找到,与本地缓存中的路由信息进行对比,判断路由信息是否发生了改变,如果未发生变化,则直接返回false
循环遍历路由信息的QueueData信息,如果队列没有写权限,则继续遍历下一个QueueData;根据brokerName找到brokerData信息,找不到或没有找到Master节点,则遍历下一个QueueData;根据写队列个数,根据topic+序号创建MessageQueue,填充topicPublishInfo的List。完成消息发送的路由查找
首先消息发送端采用重试机制,由retryTimesWhenSendFailed指定同步方式重试次数,异步重试机制在收到消息发送结构后执行回调之前进行重试。由retryTimesWhen Send-AsyncFailed指定,接下来就是循环执行,选择消息队列、发送消息,发送成功则返回,收到异常则重试。选择消息队列有两种方式。1)sendLatencyFaultEnable=false,默认不启用Broker故障延迟机制。2)sendLatencyFaultEnable=true,启用Broker故障延迟机制。
首先在一次消息发送过程中,可能会多次执行选择消息队列这个方法,lastBrokerName就是上一次选择的执行发送消息失败的Broker。第一次执行消息队列选择时,lastBrokerName为null,此时直接用sendWhichQueue自增再获取值,与当前路由表中消息队列个数取模,返回该位置的MessageQueue(selectOneMessageQueue()方法),如果消息发送再失败的话,下次进行消息队列选择时规避上次MesageQueue所在的Broker,否则还是很有可能再次失败。
该算法在一次消息发送过程中能成功规避故障的Broker,但如果Broker宕机,由于路由算法中的消息队列是按Broker排序的,如果上一次根据路由算法选择的是宕机的Broker的第一个队列,那么随后的下次选择的是宕机Broker的第二个队列,消息发送很有可能会失败,再次引发重试,带来不必要的性能损耗,那么有什么方法在一次消息发送失败后,暂时将该Broker排除在消息队列选择范围外呢?或许有朋友会问,Broker不可用后,路由信息中为什么还会包含该Broker的路由信息呢?其实这不难解释:首先,NameServer检测Broker是否可用是有延迟的,最短为一次心跳检测间隔(10s);其次,NameServer不会检测到Broker宕机后马上推送消息给消息生产者,而是消息生产者每隔30s更新一次路由信息,所以消息生产者最快感知Broker最新的路由信息也需要30s。如果能引入一种机制,在Broker宕机期间,如果一次消息发送失败后,可以将该Broker暂时排除在消息队列的选择范围中
消息发送参数详解。
1)Message msg:待发送消息。
2)MessageQueue mq:消息将发送到该消息队列上。
3)CommunicationMode communicationMode:消息发送模式,SYNC、ASYNC、ONEWAY。
4)SendCallback sendCallback:异步消息回调函数。
5)TopicPublishInfo topicPublishInfo:主题路由信息
6)long timeout:消息发送超时时间。
Step1:根据MessageQueue获取Broker的网络地址。如果MQClientInstance的brokerAddrTable未缓存该Broker的信息,则从NameServer主动更新一下topic的路由信息。如果路由更新后还是找不到Broker信息,则抛出MQClientException,提示Broker不存在。
Step2:为消息分配全局唯一ID,如果消息体默认超过4K(compressMsgBodyOverHowmuch),会对消息体采用zip压缩,并设置消息的系统标记为MessageSysFlag.COMPRESSED_FLAG。如果是事务Prepared消息,则设置消息的系统标记为MessageSysFlag.TRANSACTION_PREPARED_TYPE。
Step3:如果注册了消息发送钩子函数,则执行消息发送之前的增强逻辑。通过Defaul tMQProducerImpl#registerSendMessageHook注册钩子处理类,并且可以注册多个。简单看一下钩子处理类接口。
Step4:构建消息发送请求包。主要包含如下重要信息:生产者组、主题名称、默认创建主题Key、该主题在单个Broker默认队列数、队列ID(队列序号)、消息系统标记(MessageSysFlag)、消息发送时间、消息标记(RocketMQ对消息中的flag不做任何处理,供应用程序使用)、消息扩展属性、消息重试次数、是否是批量消息等。
Step5:根据消息发送方式,同步、异步、单向方式进行网络传输
Step6:如果注册了消息发送钩子函数,执行after逻辑。注意,就算消息发送过程中发生RemotingException、MQBrokerException、InterruptedException时该方法也会执行。
RocketMQ采取的方式是,对单条消息内容使用固定格式进行存储,如图所示。
总结:
1)消息生产者启动流程重点理解MQClientInstance、消息生产者之间的关系。
2)消息队列负载机制消息生产者在发送消息时,如果本地路由表中未缓存topic的路由信息,向Name-Server发送获取路由信息请求,更新本地路由信息表,并且消息生产者每隔30s从Name-Server更新路由表。
3)消息发送异常机制消息发送高可用主要通过两个手段:重试与Broker规避。Broker规避就是在一次消息发送过程中发现错误,在某一时间段内,消息生产者不会选择该Broker(消息服务器)上的消息队列,提高发送消息的成功率。
4)批量消息发送
RocketMQ支持将同一主题下的多条消息一次性发送到消息服务端。
RocketMQ消息存储
目前的MQ中间件从存储模型来看,分为需要持久化和不需要持久化的两种模型,现在大多数的MQ都是支持持久化存储的,比如ActiveMQ、RabbitMQ、Kafka,RocketMQ,而ZeroMQ却不需要支持持久化存储。然而业务系统也大多需要MQ有持久存储的能力,能大大增加系统的高可用性。从存储方式和效率来看,文件系统高于KV存储,KV存储又高于关系型数据库,直接操作文件系统肯定是最快的,但可靠性却是最低的,而关系型数据库的性能和可靠性与文件系统恰恰相反。
重点内容如下。
● RocketMQ存储概要设计
● 消息发送存储流程
● 存储文件组织与内存映射机制
● RocketMQ存储文件
● 消息消费队列、索引文件构建机和制
● RocketMQ文件恢复机制
● RocketMQ刷盘机制
● RocketMQ文件删除机制
存储概要设计
RocketMQ主要存储的文件包括Comitlog文件、ConsumeQueue文件、IndexFile文件。
RocketMQ将所有主题的消息存储在同一个文件中,确保消息发送时顺序写文件,尽最大的能力确保消息发送的高性能与高吞吐量。但由于消息中间件一般是基于消息主题的订阅机制,这样便给按照消息主题检索消息带来了极大的不便。为了提高消息消费的效率,RocketMQ引入了ConsumeQueue消息队列文件,每个消息主题包含多个消息消费队列,每一个消息队列有一个消息文件。IndexFile索引文件,其主要设计理念就是为了加速消息的检索性能,根据消息的属性快速从Commitlog文件中检索消息。RocketMQ是一款高性能的消息中间件,存储部分的设计是核心,存储的核心是IO访问性能,本章也会重点剖析RocketMQ是如何提高IO访问性能的。进入RocketMQ存储剖析之前,先看一下RocketMQ数据流向,如图4-1所示。
1)CommitLog:消息存储文件,所有消息主题的消息都存储在CommitLog文件中。
2)ConsumeQueue:消息消费队列,消息到达CommitLog文件后,将异步转发到消息消费队列,供消息消费者消费。
3)IndexFile:消息索引文件,主要存储消息Key与Offset的对应关系。
4)事务状态服务:存储每条消息的事务状态。
5)定时消息服务:每一个延迟级别对应一个消息消费队列,存储延迟队列的消息拉取进度。
RocketMQ主从同步(HA)机制
高可用特性是目前分布式系统中必备的特性之一,对一个中间件来说没有HA机制是一个重大的缺陷,我们将主要分析RocketMQ主从同步(HA)机制。
● 主从同步复制实现原理。
● RocketMQ读写分离机制。
为了提高消息消费的高可用性,避免Broker发生单点故障引起存储在Broker上的消息无法及时消费,RocketMQ引入了Broker主备机制,即消息消费到达主服务器后需要将消息同步到消息从服务器,如果主服务器Broker宕机后,消息消费者可以从从服务器拉取消息。
详细探讨RocketMQ HA的实现原理,RocketMQ HA核心实现类图如图:
由图可知,RocketMQ HA由7个核心类实现:
1)HAService:RocketMQ主从同步核心实现类。
2)HAService
A
c
c
e
p
t
S
o
c
k
e
t
S
e
r
v
i
c
e
:
H
A
M
a
s
t
e
r
端
监
听
客
户
端
连
接
实
现
类
。
3
)
H
A
S
e
r
v
i
c
e
AcceptSocketService:HA Master端监听客户端连接实现类。 3)HAService
AcceptSocketService:HAMaster端监听客户端连接实现类。3)HAServiceGroupTransferService:主从同步通知实现类。
4)HAService
H
A
C
l
i
e
n
t
:
H
A
C
l
i
e
n
t
端
实
现
类
。
5
)
H
A
C
o
n
n
e
c
t
i
o
n
:
H
A
M
a
s
t
e
r
服
务
端
H
A
连
接
对
象
的
封
装
,
与
B
r
o
k
e
r
从
服
务
器
的
网
络
读
写
实
现
类
。
6
)
H
A
C
o
n
n
e
c
t
i
o
n
HAClient:HA Client端实现类。 5)HAConnection:HA Master服务端HA连接对象的封装,与Broker从服务器的网络读写实现类。 6)HAConnection
HAClient:HAClient端实现类。5)HAConnection:HAMaster服务端HA连接对象的封装,与Broker从服务器的网络读写实现类。6)HAConnectionReadSocketService:HA Master网络读实现类。
7)HAConnection$WriteSocketServicce:HA Master网络写实现类
RocketMQ HA的实现原理如下:
1)主服务器启动,并在特定端口上监听从服务器的连接。
2)从服务器主动连接主服务器,主服务器接收客户端的连接,并建立相关TCP连接。
3)从服务器主动向主服务器发送待拉取消息偏移量,主服务器解析请求并返回消息给从服务器。
4)从服务器保存消息并继续发送新的消息同步请求。
RocketMQ HA主从同步机制
如图所示:
RocketMQ读写分离机制
消息消费是基于消息消费队列MessageQueue。
RocketMQ根据MessageQueue查找Broker地址的唯一依据是brokerName,从Rocket-MQ的Broker组织结构中得知同一组Broker(M-S)服务器,它们的brokerName相同但brokerId不同,主服务器的brokerId为0,从服务器的brokerId大于0, RocketMQ提供MQClient Factory.findBrokerAddressInSubscribe来实现根据brokerName、brokerId查找Broker地址。
RocketMQ根据MessageQueue查找Broker地址的唯一依据是brokerName,从Rocket-MQ的Broker组织结构中得知同一组Broker(M-S)服务器,它们的brokerName相同但brokerId不同,主服务器的brokerId为0,从服务器的brokerId大于0, RocketMQ提供MQClient Factory.findBrokerAddressInSubscribe来实现根据brokerName、brokerId查找Broker地址。
通常会根据brokerId判断主从节点,如果brokerId=0,表示返回的Broker是主节点,否则返回的是从节点。
如果主服务器繁忙则建议下一次从从服务器拉取消息,设置suggestWhichBrokerId为配置文件中whichBrokerWhenConsumeSlowly属性,默认为1。如果一个Master拥有多台Slave服务器,参与消息拉取负载的从服务器只会是其中一个。
具体大家可以看看底层代码。
总结:
RocketMQ的HA机制,其核心实现是从服务器在启动的时候主动向主服务器建立TCP长连接,然后获取服务器的commitlog最大偏移量,以此偏移量向主服务器主动拉取消息,主服务器根据偏移量,与自身commitlog文件的最大偏移量进行比较,如果大于从服务器的commitlog偏移量,主服务器将向从服务器返回一定数量的消息,该过程循环进行,达到主从服务器数据同步。RocketMQ读写分离与其他中间件的实现方式完全不同,RocketMQ是消费者首先向主服务器发起拉取消息请求,然后主服务器返回一批消息,然后会根据主服务器负载压力与主从同步情况,向从服务器建议下次消息拉取是从主服务器还是从从服务器拉取。
RocketMQ事务消息
RocketMQ 4.3.0版本,此版本解决了RocketMQ对事务的支持,这一重大更新对RocketMQ至关重要。
主要内容如下。
● 事务消息实现思想
● 事务消息发送流程
● 事务消息提交或回滚
● 事务消息回查事务状态
RocketMQ事务消息的实现原理基于两阶段提交和定时事务状态回查来决定消息最终是提交还是回滚,交互设计如图所示:
1)应用程序在事务内完成相关业务数据落库后,需要同步调用RocketMQ消息发送接口,发送状态为prepare的消息。消息发送成功后,RocketMQ服务器会回调RocketMQ消息发送者的事件监听程序,记录消息的本地事务状态,该相关标记与本地业务操作同属一个事务,确保消息发送与本地事务的原子性。
2)RocketMQ在收到类型为prepare的消息时,会首先备份消息的原主题与原消息消费队列,然后将消息存储在主题为RMQ_SYS_TRANS_HALF_TOPIC的消息消费队列中。
3)RocketMQ消息服务器开启一个定时任务,消费RMQ_SYS_TRANS_HALF_TOPIC的消息,向消息发送端(应用程序)发起消息事务状态回查,应用程序根据保存的事务状态回馈消息服务器事务的状态(提交、回滚、未知),如果是提交或回滚,则消息服务器提交或回滚消息,如果是未知,待下一次回查,RocketMQ允许设置一条消息的回查间隔与回查次数,如果在超过回查次数后依然无法获知消息的事务状态,则默认回滚消息。
消息发送流程
Step1:首先为消息添加属性,TRAN_MSG和PGROUP,分别表示消息为prepare消息、消息所属消息生产者组。设置消息生产者组的目的是在查询事务消息本地事务状态时,从该生产者组中随机选择一个消息生产者即可,然后通过同步调用方式向RocketMQ发送消息。
Step2:根据消息发送结果执行相应的操作。
1)如果消息发送成功,则记录事务消息的本地事务状态,例如可以通过将消息唯一ID存储在数据中,并且该方法与业务代码处于同一个事务,与业务事务要么一起成功,要么一起失败。这里是事务消息设计的关键理念之一,为后续的事务状态回查提供唯一依据。
2)如果消息发送失败,则设置本次事务状态为LocalTransactionState.ROLLBACK_MESSAGE。
Step3:结束事务。
根据第二步返回的事务状态执行提交、回滚或暂时不处理事务。
1)LocalTransactionState.COMMIT_MESSAGE:提交事务。
2)LocalTransactionState.COMMIT_MESSAGE:回滚事务。
3)LocalTransactionState.UNKNOW:结束事务,但不做任何处理。
Broker端在收到消息存储请求时,如果消息为prepare消息,则执行prepareMessage方法,否则走普通消息的存储流程。
事务消息与非事务消息发送流程的主要区别,如果是事务消息则备份消息的原主题与原消息消费队列,然后将主题变更为RMQ_SYS_TRANS_HALF_TOPIC,消费队列变更为0,然后消息按照普通消息存储在commitlog文件进而转发到RMQ_SYS_TRANS_HALF_TOPIC主题对应的消息消费队列。也就是说,事务消息在未提交之前并不会存入消息原有主题,自然也不会被消费者消费。既然变更了主题,RocketMQ通常会采用定时任务(单独的线程)去消费该主题,然后将该消息在满足特定条件下恢复消息主题,进而被消费者消费。
事务消息发送如下图:
提交或回滚事务
根据消息所属的消息队列获取Broker的IP与端口信息,然后发送结束事务命令,其关键就是根据本地执行事务的状态分别发送提交、回滚或“不作为”的命令
如果结束事务动作为提交事务,则执行提交事务逻辑,其关键实现如下:
1)首先从结束事务请求命令中获取消息的物理偏移量(commitlogOffset)。
2)然后恢复消息的主题、消费队列,构建新的消息对象。
3)然后将消息再次存储在commitlog文件中,此时的消息主题则为业务方发送的消息,将被转发到对应的消息消费队列,供消息消费者消费。
4)消息存储后,删除prepare消息,其实现方法并不是真正的删除,而是将prepare消息存储到RMQ_SYS_TRANS_OP_HALF_TOPIC主题中,表示该事务消息(prepare状态的消息)已经处理过(提交或回滚),为未处理的事务进行事务回查提供查找依据。
事务的回滚与提交的唯一差别是无须将消息恢复原主题,直接删除prepare消息即可,同样是将预处理消息存储在RMQ_SYS_TRANS_OP_HALF_TOPIC主题中,表示已处理过该消息。
事务消息回查事务状态
事务消息存储在消息服务器时主题被替换为RMQ_SYS_TRANS_HALF_TOPIC,执行完本地事务返回本地事务状态为UN_KNOW时,结束事务时将不做任何处理,而是通过事务状态定时回查以期得到发送端明确的事务操作(提交事务或回滚事务)。
事务的过期时间,只有当消息的存储时间加上过期时间大于系统当前时间时,才对消息执行事务状态回查,否则在下一次周期中执行事务回查操作。
事务回查最大检测次数,如果超过最大检测次数还是无法获知消息的事务状态,RocketMQ将不会继续对消息进行事务状态回查,而是直接丢弃即相当于回滚事务。
RocketMQ处理任务的一个通用处理逻辑就是为每个任务一次只分配某个固定时长,超过该时长则需等待下次任务调度。RocketMQ为待检测主题RMQ_SYS_TRANS_HALF_TOPIC的每个队列做事务状态回查,一次最多不超过60秒,目前该值不可配置。
如果该消息已被处理,则继续处理下一条消息。
根据消息队列偏移量从消费队列中获取消息。
从待处理任务队列中拉取消息,如果未拉取到消息,则根据允许重复次数进行操作,默认重试一次,目前不可配置。
判断该消息是否需要discard(吞没、丢弃、不处理)或skip(跳过)。
如果消息指定了事务消息过期时间属性(PROPERTY_CHECK_IMMUNITY_TIME_IN_SECONDS),如果当前时间已超过该值。
如果当前时间还未过(应用程序事务结束时间),则跳出本次处理,等下一次再试。
异步处理的,无法立刻知道其处理结果,为了避免简化prepare消息队列和处理队列的消息消费进度处理,先存储,然后消费进度向前推动,重复发送的消息在事务回查之前会判断是否处理过。另外一个目的就是需要修改消息的检查次数,RocketMQ的存储设计采用顺序写,去修改已存储的消息,其性能无法高性能。
发送具体的事务回查命令,使用线程池来异步发送回查消息,为了回查消费进度保存的简化,只要发送了回查消息,当前回查进度会向前推动,如果回查失败,上一步骤新增的消息将可以再次发送回查消息,那如果回查消息发送成功,会不会下一次又重复发送回查消息呢?这个可以根据OP队列中的消息来判断是否重复,如果回查消息发送成功并且消息服务器完成提交或回滚操作,这条消息会发送到OP队列中,然后首先会根据处理进度获取一批已处理的消息,来与消息判断是否重复,由于一次只拉32条消息,那又如何保证一定能拉取到与当前消息的处理记录呢?如果此批消息最后一条未超过事务延迟消息,则继续拉取更多消息进行判断, OP队列也会随着回查进度的推进而推进。
如果无法判断是否发送回查消息,则加载更多的已处理消息进行筛选。
保存(Prepare)消息队列的回查进度。
保存处理队列(OP)的进度。
总结:
RocketMQ事务消息的实现机制,一言以蔽之,RocketMQ事务消息基于两阶段提交和事务状态回查机制来实现,所谓的两阶段提交,即首先发送prepare消息,待事务提交或回滚时发送commit、rollback命令。再结合定时任务,RocketMQ使用专门的线程以特定的频率对RocketMQ服务器上的prepare信息进行处理,向发送端查询事务消息的状态来决定是否提交或回滚消息。
应用场景分析
随着互联网技术蓬勃发展、微服务架构思想的兴起,系统架构追求小型化、轻量化,原有的大型集中式的IT系统通常需要进行垂直拆分,孵化出颗粒度更小的众多小型系统,因此对系统间松耦合的要求越来越高,目前RPC、服务治理、消息中间件几乎成为互联网架构的标配。引入消息中间件,服务之间可以通过可靠的异步调用,从而降低系统之间的耦合度,提高系统的可用性。消息中间件另一个重要的应用场景是解决系统之间数据的一致性(最终一致性)。
消息中间件的两大核心点:异步与解耦。
经典应用场景:数据同步。
某公司的IT系统由多个子系统构成,例如基础数据平台、社交平台、订单平台。各个子系统之间使用单独的数据库,这样就面临基础数据的管理,比如用户信息表、行政区域表,按照职责划分的话这些数据是在基础数据平台进行维护的,但各个子系统的业务表需要关联这些基础数据,所有基础数据的表结构也会同时存在各个子系统之间,此时数据的同步如何解决呢?
解决方案1:基础平台数据发送变化后,通过调用社交平台、订单平台提供的数据同步接口完成数据同步。
缺点:基础平台必须依赖各个业务子系统,当业务系统增加时,需要修改基础平台同步代码或配置文件,基础平台与各个业务子系统强耦合。
解决方案2:引入消息中间件,基础平台在数据发送变化后,发送一条消息到消息服务器,然后就正常返回。各个业务系统订阅数据同步消息主题,自己负责消息消费,并依靠MQ提供的消息重试机制方便异常处理。