RocketMQ介绍与实现原理
- 1、简介
- 2、服务发现
- 2.1 路由注册
- 2.2 路由删除
- 2.3 路由发现
- 2.4 总结
- 3、RocketMQ消息发送
- 3.1 消息发送基本流程
- 3.1.1 消息批量发送
- 3.1.2 消息发送异常机制
- 4、RocketMQ消息存储
- 4.1 消息发送存储流程
- 4.2 过期文件删除机制
- 5、消息的消费
- 5.1 消息拉取
- 5.1.1 消息拉取基本流程
- 5.1.1.1 消息拉取客户端消息拉取请求封装
- 5.1.1.2 消息服务器Broker组装消息
- 5.1.1.3 消息拉取客户端处理消息
- 5.1.2 消息拉取长轮询机制
- 5.1.3 消息队列负载与重新分布机制(待续)
- 5.2 消息消费过程
- 5.3 定时消息机制
- 5.3.1 定时调度逻辑
- 6 高可用
- 7 RocketMQ事务消息
1、简介
RocketMQ采用发布订阅模式,基本参与组件主要包括:消息发送者、消息服务器(消息存储)、消息消费、路由发现。
功能:
(1) RocketMQ可以严格保证消息有序
(2)RocketMQ支持消息过滤(消息消费时可以对同一主题下的消息按照规则只消费自己感兴趣的消息)。
(3)RocketMQ引入内存映射机制,所有主题的消息顺序存储在同一个文件中。有消息文件过期机制和文件存储空间报警机制。
(4)RocketMQ在不发生消息堆积时,以长轮询模式实现准实时的消息推送模式。
(5)确保消息必须被消费一次,有重复消费的可能。
(6)支持回溯消息(已消费的消息可以按时间回溯)
(7)不支持任意进度的定时消息,支持特定延迟级别。
2、服务发现
RocketMQ摒弃了业界常用的使用zookeeper充当信息管理的“注册中心”,而是自研NameServer来实现元数据的管理(Topic路由信息等)。
Broker 消息服务器在启动的时候向所有NameServer注册,消息生产者在发送消息之前先从NameServer获取Broker服务器地址列表,根据负载算法从列表中选择一台消息服务器进行消息发送。
NameServer的启动类是NamesrvStartup, 通过执行这个类的main方法可以启动一个NameServer
package org.apache.rocketmq.namesrv;
public class NamesrvStartup {
public static void main(String[] args) {main0(args);}
public static NamesrvController main0(String[] args) {
...
NamesrvController controller = createNamesrvController(args);
start(controller); // start里面调用了initialize()和start()
...
}
}
package org.apache.rocketmq.namesrv;
public class NamesrvController {
public boolean initialize() {
// 加载配置
this.kvConfigManager.load();
// 初始化netty server 连接就靠它
this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
this.remotingExecutor = Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
this.registerProcessor();
// 定时任务,每隔10s扫一次 brokerLiveTable
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
NamesrvController.this.routeInfoManager.scanNotActiveBroker();
}
}, 5, 10, TimeUnit.SECONDS);
...
}
}
路由元数据都存到RouteInfoManager。
public class RouteInfoManager {
private static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
private final static long BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
}
2.1 路由注册
RocketMQ路由注册通过Broker与NameServer的心跳功能实现。Bocker启动时向集群所有的NameServer发送心跳语句,每隔30s向集群中所有NameServer发送心跳包,NameServer收到Bocker心跳包会更新brokerLiveTable缓存中的BrokerLiveInfo的lastUpdateTimestamp, 然后NameServer每隔10s扫描brokerLiveTable, 如果连续120s没有收到心跳包,NameServer将移除Broker的路由信息同时关闭Socket连接。
发心跳包的代码在: org.apache.rocketmq.broker.BrokerController#start 一样是启动一个定时任务,每隔30s, 遍历nameServer列表逐个发消息。(网络都是基于netty)
NameServer的DefaultRequestProcessor收到网络请求后,如果是broker的心跳,会调用RouteInfoManager#registerBroker。 代码也比较简单,先加锁,更新元数据信息(上面的RouteInfoManager里面的一些hashmap)。
2.2 路由删除
RocketMQ有两个触发点来触发路由删除
- NameServer定时扫描brokerLiveTable检测上次心跳包与当前系统时间的时间差,大于120s,则删除。
- Broker正常关闭,调用unregisterBroker
删除的逻辑一样,都是从元数据中移除broker相关的信息.
2.3 路由发现
RocketMQ路由发现是非实时的,当Topic路由出现变化后,NameServer不主动推送给客户端,而是由客户端定时拉取主题最新的路由。
org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#getRouteInfoByTopic
2.4 总结
3、RocketMQ消息发送
RocketMQ 支持3种消息发送方式:同步(sync)、异步(async)、单向(oneway)。
- 同步:发送者向MQ执行发送消息API时,同步等待,直到消息服务器返回发送结果。
- 异步:发消息时,指定发送成功回调函数,调用发送消息API,立即返回,消息发送者线程不阻塞。消息发送成功或失败的回调任务在一个新线程中执行。
- 单向:发送消息不注册监听,也不等待结果。(只管发,不在于消息是否成功存储在消息服务器上)
可以看到,RocketMQ的消息包含了主题topic、flag、properties扩展属性、body消息体
public class Message implements Serializable {
private static final long serialVersionUID = 8445773977080406428L;
private String topic;
private int flag;
private Map<String, String> properties;
private byte[] body;
private String transactionId;
}
properties里面包含:
- tag: 消息TAG,用于消息过滤
- keys: 消息索引键,多个空格隔开
- waitStoreMsgOK:消息发送时是否等消息存储完成后再返回
- delayTimeLevel:消息延迟级别,用于定时消息或消息重试
3.1 消息发送基本流程
消息发送流程主要步骤:验证消息、查找路由、消息发送(包含异常处理机制)
1)验证消息
消息发送之前,首先确保生产者处于运行状态,然后主题名称、消息体不能为空、消息长度不能等于0且默认不能超过4M
2)查找路由
如果生产者中缓存了topic的路由信息,且该路由信息中包含了消息队列,直接返回该路由信息。反之向NameServer查询该topic的路由信息。最终没有找到,会抛异常。
org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#tryToFindTopicPublishInfo
3)消息发送
消息发送的核心方法:org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendKernelImpl
private SendResult sendKernelImpl(final Message msg, // 待发送的消息
final MessageQueue mq, // 消息发送到该队列上
final CommunicationMode communicationMode, // 消息发送模式 SYNC、ASYNC、ONEWAY
final SendCallback sendCallback, // 异步消息回调函数
final TopicPublishInfo topicPublishInfo, // 主题路由信息
final long timeout) // 超时时间
- 从MessagQueue中获取Broker的网络地址
- 为消息分配唯一ID,如果消息体大于4k,会压缩(zip)
- 执行消息发送之前的增强逻辑(如果有注册消息发送钩子函数的话)
- 构建消息发送请求包(包括:生产者组、主题名称、队列ID、重试次数、是否批量等)
- 根据消息发送方式,进行网络传输。
- 执行after逻辑(如果注册了钩子函数)。就算消息发送过程发生RemotingException、MQBrokerException、InteruptedException时该方法也会执行。
3.1.1 消息批量发送
消息批量发送的流程和上述单条消息流程一致。对于批量发送消息,要解决的消息的编码和解码问题。批量的消息也是存于body中。RocketMQ会对单条消息使用固定格式存储。方便服务端能正确解析出消息。
3.1.2 消息发送异常机制
针对异常,RocketMQ有重试与Broker规避机制。Broker规避就是在一次消息发送过程中发现错误,在某一时间段内,消息生产者不会选择该Broker(消息服务器)上的消息队列,提高发送消息的成功率。
4、RocketMQ消息存储
主要存储的文件包括:Commitlog文件、ConsumeQueue文件、IndexFile文件.
Rocket所有消息顺序的写在Comitlog中,确保高性能。但由于消息中间件一般是基于消息主题的订阅机制,顺序写机制给基于主题的检索带来不便。RocketMQ引入了ConsumeQueue消息队列文件,每个主题包含多个消息队列,每个队列是一个文件。IndexFile索引文件主要是为了加速消息的检索性能,根据消息的属性快速从Commitlog文件中检索消息。
- Comitlog:消息存储文件,所有主题的消息都会存储在commitLog文件中。
- ConsumeQueue:消息消费队列,消息到达commitlog文件后,将异步转发到消息消费队列,供消息消费者消费。
- IndexFile:消息索引文件,主要存储消息Key与Offset的对应关系。
- 事务状态服务:存储每条消息的事务状态。
- 定时消息服务:每个延迟级别对应的消息消费队列,存储延迟队列的消息拉取进度。
4.1 消息发送存储流程
消息存储入口:
org.apache.rocketmq.store.DefaultMessageStore#putMessage
调用
org.apache.rocketmq.store.CommitLog#putMessage
- 如果当前 Broker停止工作或Broker为SLAVE角色或当前Rocket不支持写入则拒绝消息写入;如果消息主题长度超过127个字符、消息属性长度超过32767字符将拒绝写入消息。
- 如果消息延迟级别大于0,将消息的原主题名称与原消息队列ID存入消息属性中,用延迟消息主题SCHEDULE_TOPIC、消息队列ID更新原先消息的主题与队列。(这个是并发消息消费重试的关键)
- 获取当前可以写入的Commitlog文件。(主题消息顺序写入commitLog, 文件默认最大1G,写满一个再创建另一个。)
- 写入CommitLog之前,先申请lock, 消息存储到CommitLog文件是串行的。
- 设置消息存储时间。如果mappedFile(CommitLog类mappedFileQueue属性里面的元素)为空,表明这个消息是第一次消息发送,用偏移量0创建第一个commit文件。文件名是00000000000000000000.
- 将消息追加到MapedFile中。
- 创建全局唯一消息ID(消息ID组成16个字节的数组:4字节的ip + 4字节的端口号 + 8字节的消息偏移量;为了消息ID的可读性,返回给应用程序的msgId会转化成字符类型。内部在使用的时候又会转回字节数组,从而根据消息偏移量快速找到对应的消息内容)。
- 获取该消息在消息队列的偏移量。CommitLog中保存了当前所有消息队列的当前待写入偏移量。
- 根据消息体的长度、主题长度、属性的长度结合消息存储格式计算出消息的总长度。
- 如果总长度+8字节(每个CommitLog文件会留8个字节,高4位存当前文件剩余空闲空间,第四位魔数)大于CommitLog文件的空闲,Broker会重新创建一个文件。文件名是偏移量,高位补零。
- 将消息存储到MappedFile对应的ByteBuffer中(还没有刷到磁盘)
- 更新消息队列偏移量
- 处理完消息追加逻辑后释放锁。
RocketMQ的存储是基于JDK NIO的内存映射机制(MapperByteBuffer)的,消息存储时首先将消息追加到内存,再根据配置的刷盘策略在不同时间进行刷写磁盘。默认是异步刷盘,RocketMQ使用一个单独的线程按照某个设定的频率执行刷盘操作。(索引文件每次更新就会将上一次改动刷写到磁盘)
4.2 过期文件删除机制
由于RocketMQ CommitLog、ConsumeQueue文件是基于内存的映射机制并在启动的时候会加载commitLog、consumequeue目录下的所有文件,为了避免内存与磁盘的浪费,所以需要引入一种机制来删除已过期的文件。
RocketMQ顺序写文件,所有的写操作都会落到最后一个CommitLog、ConsumeQueue文件上,之前的文件的文件在下一个文件创建后不会被更新。
如果非当前写文件在一定时间间隔内没有再次被更新,RocketMQ会认为是过期文件,可以被删除。默认每个文件的删除时间是72小时。(RocketMQ不会关注这个文件上的消息是否全部被消费)。
5、消息的消费
消息消费以组的模式展开,一个消费组内可以包含多个消费者,每个消费组可订阅多个主题,消费组直接有集群模式和广播模式两种消费模式。集群模式,主题下的同一条消息只允许被其中的一个消费者消费。广播模式,主题下的同一条消息将被集群内所有消费者消费一次。
消息服务器与消费者之间的消息传送方式也有两种方式:推模式(消息由消息服务器推送给消息消费者)、拉模式(消费端主动发起拉消息请求)。RocketMQ消息推模式的实现基于拉模式,在拉模式上包装一层,一个拉取任务完成后开始下一个拉取任务。
集群模式,消息队列负载机制遵循一个通用的思想:一个消息队列同一时间只允许被一个消费者消费,一个消费者可以消费多个消息队列。
RocketMQ支持同一消息队列上的消息顺序消费,不支持消息全局顺序消费。如果要实现某一主题的全局顺序消息消费,可以将该主题的队列数设置为1,牺牲高可用性。(这里为什么是牺牲高可用,不同队列的消息也是不一样的吧?)
RocketMQ支持两种消息过滤模式:表达式(TAG、SQL92)与类过滤模式。
5.1 消息拉取
RocketMQ使用一个单独的线程PullMessageService来负责消息的拉取。
package org.apache.rocketmq.client.impl.consumer;
public class PullMessageService extends ServiceThread { // implements Runnable
@Override
public void run() {
while (!this.isStopped()) {
PullRequest pullRequest = this.pullRequestQueue.take();
this.pullMessage(pullRequest);
}
}
......
}
可以看到消息拉取的请求参数应该在PullRequest , 以下是PullRequest核心属性
package org.apache.rocketmq.client.impl.consumer;
public class PullRequest {
private String consumerGroup; // 消费者组
private MessageQueue messageQueue; // 待拉取消费队列
private ProcessQueue processQueue; // 消息处理队列, 从Broker拉取到的消息先存入ProcessQueue,然后再提交到消费者消费线程池消费
private long nextOffset; // 待拉取的MessageQueue偏移量
private boolean lockedFirst = false; // 是否被锁定
}
ProcessQueue是MessageQueue在消费端的重现、快照。PullMessageService从消费服务器默认每次拉取32条消息,按照消息的队列偏移量顺序存放在ProcessQueue中,PullMessageService然后将消息提交到消费者消费线程池,消息成功消费后从ProcessQueue中移除。
5.1.1 消息拉取基本流程
消息拉取分为3个主要步骤:
1)消息拉取客户端消息拉取请求封装。
2)消息服务器查找并返回消息。
3)消息拉取客户端处理返回的消息。
5.1.1.1 消息拉取客户端消息拉取请求封装
- 从PullRequest中获取ProcessQueue,如果处理队列当前状态未被丢弃,更新ProcessQueue的lastPullTimestamp为当前时间戳。
- 进行消息拉取流控。从消息消费数量与消费间隔两个维度进行控制。
- 拉取该主题订阅信息。
- 构建消息拉取系统标记。(PullSysFlag)
- 调用PullApiWrapper.pullKernelImpl方法与服务端交互。
1)MessageQueue mq :从哪个消息消费队列拉取消息)
2)String subExpression:消息过滤表达式。
3)String expressionType:消息表达式类型,分为TAG、SQL92。
4)long offset:消息拉取偏移量。
5)int maxNums:本次拉取最大消息数,默认32条。
6)int sysFlag:拉取系统标记。
7)long commitOffset:当前MessageQueue的消费进度(内存中)。
8)long brokerSuspendMaxTimeMillis:消息拉取过程中允许Broker挂起时间,默认15s
9)long timeoutMillis:消息拉取超时时间。
10)CommunicationMode communicationMode:消息拉取模式,默认为异步拉取。
11)PullCallback pullCallback:从Broker拉取到消息后的回调方法。 - 根据brokerName、BrokerId从MQClientInstance中获取Broker地址,在整个RocketMQ Broker的部署结构中,相同名称的Broker构成主从结构,其BrokerId会不一样,在每次拉取消息后,会给出一个建议,下次拉取从主节点还是从节点拉取。
- 如果消息过滤模式为类过滤,需要根据主题名称、broker地址找到注册在Broker上的FilterServer地址,从FilterServer上拉取消息,否则从Broker上拉取消息。
上述步骤完成后,RocketMQ通过MQClientAPIImpl#pullMessageAsync方法异步向Broker拉取消息
5.1.1.2 消息服务器Broker组装消息
服务端处理消息拉取到入口:org.apache.rocketmq.broker.processor.PullMessageProcessor
- 根据订阅信息,构建消息过滤器。
- 调用MessagesStore.getMessage查找消息。
- 根据主题名称与队列编号获取消息消费队列。
- 如果待拉取偏移量没有异常(大于minOffset且小于maxOffset),从当前offset处尝试拉取32条消息。
- 根据PullResult填充responseHeader的nextBegionOffset、minOffset、maxOffset。
- 根据主从同步延迟,如果从节点数据包含下一次拉取的偏移量,设置下一次拉取任务的brokerId。
- 设置resposne
- 如果commitlog标记可用并且当前节点为主节点,则更新消息消费进度。
5.1.1.3 消息拉取客户端处理消息
消息拉取客户端调用入口:MQClientAPIImpl#pullMessageAsync,NettyRemotingClient在收到服务器响应结构后会回调PullCallback的onSuccess或者onException, PullCallBack对象在pullMessage方法中创建。
- 根据响应结果解码成PullResultExt对象,此时只是从网络中读取消息列表到byte[] messageBinary属性。
- 调用pullAPIWrapper的processPullResult将消息字节数组解码成消息列表填充msgFoundList,并对消息进行消息过滤(TAG)模式。
public class PullResult {
private final PullStatus pullStatus; // 拉取结果
private final long nextBeginOffset; // 下次拉取偏移量
private final long minOffset; // 消息队列最小偏移量
private final long maxOffset; // 消息队列最大偏移量
private List<MessageExt> msgFoundList; // 具体拉取的消息列表
}
- 更新PullRequest的下一次拉取偏移量,如果msgFoundList为空,则立即将PullRequest放入到PullMessageService的pullRequestQueue,以便PullMessageService能及时唤醒并再次执行消息拉取。
- 首先将拉取到的消息存入ProcessQueue,然后将拉取到的消息提交到ConsumeMessageService中供消费者消费,该方法是异步方法。
- 将消息提交给消费者线程后PullCallBack将立即返回,本次消息拉取完成。根据pullInterval参数,如果pullInterval> 0 , 则等待pullInterval毫秒后将PullRequest对象放入到PullMessageService的pullRequestQueue中,该消息队列的下次拉取即将被激活,达到持续消息拉取,实现准实时拉取消息的效果。
5.1.2 消息拉取长轮询机制
RocketMQ 并没有真正实现推模式,而是消费者主动向消息服务器拉取消息,RocketMQ推模式是循环想消息服务器发送消息拉取请求,如果消息消费者向RocketMQ发送消息拉取时,消息并未到达消费队列,如果不启用长轮询机制,则会在服务端等待shortPollingTimeMills时间后(挂起)再去判断消息是否已到达消息队列,如果消息未到达则提示消息拉取客户端PULL_NOT_FOUNT;如果开启长轮询模式,RocketMQ一方面会每5s轮询检查一次消息是否可达,同时一有消息到达后立马通知挂起线程再次验证新消息是否是自己感兴趣的消息,如果是则从commitlog文件提取消息返回给消息拉取客户端,否则直到挂起超时。超时时间由消息拉取方在消息拉取时封装在请求参数中,PUSH模式默认为15s,PULL模式通过DefaultMQPullConsumer#setBrokerSuspendMaxTimeMillis设置。RocketMQ通过在Broker端配置longPollingEnable为true来开启长轮询模式。
RocketMQ轮询机制由两个线程共同来完成。
1)PullRequestHoldService:每隔5s重试一次
2)DefaultMessageStore#ReputMessageService,每处理一次重新拉取,Thread.sleep(1),继续下一次检查。
5.1.3 消息队列负载与重新分布机制(待续)
一个topic可以有多个消费队列是,RocketMQ默认提供5种分配算法。
- AllocateMessageQueueAveragely:平均分配
如果有5个消费队列(q1, q2, q3, q4, q5),2个消费者(c1, c2)。消息队列分配如下:
c1:q1, q2, q3
c2:q4, q5 - AllocateMessageQueueAveragelyByCircle:平均轮询分配,如上例子,分配如下:
c1:q1, q3, q5
c2: q2, q4 - AllocateMessageQueueConsistentHash:一致性hash
- AllocateMessageQueueByConfig:根据配置,为每一个消费者配置固定的消息队列。
- AllocateMessageQueueByMachineRoom:根据Broker部署机房名,对每个消费者负责不同的Broker上的队列。
下图是PullMessageService线程与RebalanceService线程交互图
5.2 消息消费过程
RocketMQ使用ConsumeMessageService来实现消息消费的处理逻辑,支持顺序消费与并发消费。
前面说到从服务器拉取消息后会回调PullCallBack回调方法,将消息放入到ProcessQueue中,然后把消息提交到消费线程池中执行,也就是调用ConsumeMessageService#submitConsumeRequest开始进入到消息消费的过程。
- 进入具体消费时,会先检查processQueue的dropped, 如果设置为true, 停止该队列消费,在进行消息重新负载时,如果该消息队列被分配给消费组内其他消费者后,需要droped设置为true, 阻止消费者继续消费不属于自己的消息队列。
- 执行钩子函数(通过consumer.getDefaultMQPushConsumerImpl().registerConsumeMessageHook()注册)
- 恢复重试消息主题名(前面讲过,如果消息延迟级别delayTimeLevel>0 会设置主题名SCHEDULE_TOPIC)
- 执行具体的消费,调用应用程序消息监听器的consumeMessage方法,进入具体的消息消费业务逻辑,返回该批消息的消费结果。最终将返回CONSUME_SUCCESS或RECONSUME_LATERR
- 执行业务消息消费后,在处理结果前再验证以下ProcessQueue的isDroped状态。如果是true的话,什么都不做,也就是说,消息会被重复消费。
- 根据消息监听器返回的结果,计算ackIndex,如果返回CONSUME_SUCCESS, ackIndex设置为msgs.size()-1, 如果返回RECONSUME_LATER, ackIndex = -1, 这是为下文发送msg back (ACK)消息做准备。
- 如果是广播模式,业务方返回RECONSUME_LATER, 消息并不会重新被消费,只是以警告级别输出到日志文件。如果是集群模式,也无妨返回RECONSUME_LATER时,该批消息需要发ACK消息。如果ACK消息发送成功,该消息会延迟消费。
- 从ProcessQueue中移除这批消息。
5.3 定时消息机制
定时消息是指消息发送到Broker后,并不立即被消费者消费而是要等到特定的时间后才能被消费。
RocketMQ不支持任意时间精度,消息延迟级别在Broker端通过messageDelayLevel配置,默认为“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”, delayLevel = 1 表示延迟1s, delayLevel = 2 表示延迟5s, 以此类推。
消息重试是通过定时实现的,定时消息实现类为org.apache.rocketmq.store.schedule.ScheduleMessageService。该类的实例在DefaultMessageStre中创建,通过在DefaultMessageStore中调用laod方法加载并调用start方法进行启动。
- 根据不同的延迟级别,创建定时任务。(例如默认是18个延迟级别,会创建18个定时任务)。每个延迟级别对应一个消息消费队列。延迟级别与消息队列的映射关系为消息队列ID = 延迟级别 - 1
定时消息的第一个设计关键点是,定时消息单独一个主题SCHEDULE_TOPIC_XXXX, 该主题下队列数量等于配置的延迟级别数量。对应关系是queueId等于延迟级别减1.ScheduleMessageService为每隔延迟级别创建一个定时Timer根据延迟级别对应的延迟时间进行延迟调度。在消息发送时,如果消息的延迟级别delayLevel大于0,将消息的原主题名称、队列ID存入消息的属性中,然后改变消息的主题、队列与延迟主题与延迟主题所属队列,消息将最终发到延迟队列的消费队列。
- 创建定时任务,每隔10s持久化一次延迟队列的消息消费进度,持久化频率通过flushDelayOffsetInterval设置。
5.3.1 定时调度逻辑
- 根据队列ID与延迟主题查找消息消费队列,如果未找到,忽略。
- 根据offset从消息消费队列中获取当前队列中所有有效的消息。如果未找到,更新以下延迟队列定时拉取进度并创建定时任务待下一次继续尝试。
- 遍历ConsumeQueue,每个标准ConsumeQueue条目为20个字节,解析出消息物理偏移量、消息长度、消息tag hashcode, 为从commitlog加载具体的消息做准备。
- 根据消息物理偏移量与消息大小从commitlog文件中查找消息。如果未找到消息,打印错误日志,根据延迟时间创建下一个定时器。
- 根据消息重新构建新的消息对象,清除消息的延迟级别属性(delayLevel)、恢复消息的主题与消费队列。
- 将消息再次存到commitlog,并转发到主题对应的队列上,供消费者再次消费。
- 更新延迟队列拉取进度。
定时消息第二个设计关键点:消息存储时如果消息的延迟级别属性delayLevel大于0,则会备份原主题、原队列到消息属性中,通过为不同的延迟级别创建不同的调度任务,当时间到达后执行调度任务,调度任务主要就是根据延迟拉取消息消费进度从延迟队列中拉取消息,然后从commitlog中加载完整消息,清除延迟级别属性并恢复原先的主题、队列,再次创建一条新的消息存入到commitlog中并转发到消息队列供消息消费者消费。
6 高可用
为了避免Broker发生单点故障引起存储在Broker上的消息无法及时消费,RocketMQ引入Broker主备机制。消息到达主服务器后需要将消息同步到消息从服务器,如果从服务器Broker宕机, 消息消费者可以从从服务器拉取消息。
实现:
- 主服务器启动,在特定端口上监听从服务器的连接
- 从服务器主动连接主服务器,主服务器接收客户端的连接,建立相关TCP连接。
- 从服务器主动向主服务器发送待拉取消息偏移量,主服务器解析请求并返回消息给从服务器。
- 从服务器保存消息并继续发送新的消息同步请求。
7 RocketMQ事务消息
RocketMQ事务消息的实现原理基于两阶段提交和定时事务状态回查来决定消息最终是提交还是回滚。
本文为读书笔记,内容大多来自《RocketMQ技术内幕 RocketMQ架构设计与实现原理》