RocketMQ 的消息交互源码解析

创建Topic的时候为何要指定MessageQueue数量?

  • 简单来说,就是你要指定你的这个Topic对应了多少个队列,也就是多少个MessageQueue。
  • MessageQueue就是RocketMQ中非常关键的一个数据分片机制,他通过MessageQueue将一个Topic的数据拆分为了很多个数据分片,然后在每个Broker机器上都存储一些MessageQueue。
  • Topic是一个逻辑上的概念,实际上在每个broker上以queue的形式保存,也就是说每个topic在broker上会划分成几个逻辑队列,每个逻辑队列保存一部分消息数据,但是保存的消息数据实际上不是真正的消息数据,而是指向commit log的消息索引。

生产者发送消息的时候写入哪个MessageQueue?

  • 生产者会跟NameServer进行通信获取Topic的路由数据, 以生产者从NameServer中就会知道,一个Topic有几个MessageQueue,哪些MessageQueue在哪台Broker机器上
  • 让一个Topic中的数据分散在多个MessageQueue中,进而分散在多个Broker机器上,实现RocketMQ集群分布式存储海量的消息数据了

如果某个Broker出现故障该怎么办?

  • 如果某个Broker临时出现故障了,比如Master Broker挂了,此时正在等待的其他Slave Broker自动热切换为Master Broker,那么这个时候对这一组Broker就没有Master Broker可以写入了
  • 如果你还是按照之前的策略来均匀把数据写入各个Broker上的MessageQueue,那么会导致你在一段时间内,每次访问到这个挂掉的Master Broker都会访问失败,这个似乎不是我们想要的样子。
  • 对于这个问题,通常来说建议大家在Producer中开启一个开关,就是sendLatencyFaultEnable
  • 一旦打开了这个开关,那么他会有一个自动容错机制,比如如果某次访问一个Broker发现网络延迟有500ms,然后还无法访问,那么就会自动回避访问这个Broker一段时间,比如接下来3000ms内,就不会访问这个Broker了

RocketMQ 是如何持久化消息的

1、为什么Broker数据存储是最重要的一个环节

  • roker数据存储实际上才是一个MQ最核心的环节,他决定了生产者消息写入的吞吐量,决定了消息不能丢失,决定了消费者获取消息的吞吐量,这些都是由他决定的

2、CommitLog消息顺序写入机制

  • 当生产者的消息发送到一个Broker上的时候,他接收到了一条消息,接着他会对这个消息做什么事情?首先第一步,他会把这个消息直接写入磁盘上的一个日志文件,叫做CommitLog,直接顺序写入这个文件
  • 这个CommitLog是很多磁盘文件,每个文件限定最多1GB,Broker收到消息之后就直接追加写入这个文件的末尾,就跟上面的图里一样。如果一个CommitLog写满了1GB,就会创建一个新的CommitLog文件。

RocketMq是如何写入数据的

设定一个topic -> 根据设定的MessageQueue个数 -> 分不在不同的master Broker里边 -> 每个MessageQueue是由多个 CommitLog组成 -> Commit是采用顺序读写。加上OS PageCache来保证写入性能 -> 首先。OS PageCache是基于内存的缓冲池。采用异步刷盘或者同步刷盘顺序写入磁盘 (异步刷盘宕机是会有可能导致数据丢失的

  • DLedger 相当于替换了 CommitLog
  • DLedger CommitLog 来构建出机器上的MessageQueue
  • Broker机器刚刚启动的时候,就是靠这个DLedger基于Raft协议实现的leader选举机制,互相投票选举出来一个Leader,其他人就是Follower,然后只有Leader可以接收数据写入,Follower只能接收Leader同步过来的数据
  • DLedger收到一条数据之后,会标记为uncommitted状态,然后他会通过自己的DLedgerServer组件把这个uncommitted数据发送给Follower Broker的DLedgerServer。
  • 接着Follower Broker的DLedgerServer收到uncommitted消息之后,必须返回一个ack给Leader Broker的DLedgerServer,然后如果Leader Broker收到超过半数的Follower Broker返回ack之后,就会将消息标记为committed状态。
  • 也就是说。当leaderBroker收到消息之后会同步给 FollowerBroker 节点。当节点响应ack之后主节点才会返回给生产者ack

源码索引

消息发送源码解析

  • 消息发送
  • producer.send(msg);
  • -> defaultMQProducerImpl.sendDefaultImpl
    • -> this.tryToFindTopicPublishInfo 从 NameService 获取 Topic路由信息(本地有缓存就从缓存中获取)
    • -> this.selectOneMessageQueue 选择一个消息队列 queue
    • -> this.sendKernelImpl 调用发送核心方法
    • -> mQClientFactory.getMQClientAPIImpl().sendMessage 进行发送
  • -> MQClientAPIImpl#sendMessageSync
  • -> remotingClient.invokeSync 调用netty方法发送 RequestCode.SEND_MESSAGE 消息
  • broker接受到消息的处理
  • -> NettyServerHandler#channelRead0
  • -> NettyRemotingAbstract#processMessageReceived
  • -> NettyRemotingAbstract#processRequestCommand 处理客户端的请求消息
  • -> processor.asyncProcessRequest 客户端发送的是异步消息,不需要同步返回成功
  • -> SendMessageProcessor#asyncProcessRequest 进入消息处理
  • -> AbstractSendMessageProcessor#parseRequestHeader 解析请求
  • -> SendMessageProcessor#asyncSendMessage 异步保存发送的消息
  • -> this.brokerController.getMessageStore().asyncPutMessage(msgInner) MessageStore存储消息
  • -> MessageStore#asyncPutMessage 异步保存发送的消息
  • -> MessageStore#putMessage 保存发送的消息
  • -> DefaultMessageStore#asyncPutMessages DefaultMessageStore保存消息默认实现
  • -> this.commitLog.asyncPutMessages(messageExtBatch) 保存发送的消息
  • -> CommitLog#asyncPutMessages 保存发送的消息
  • -> mappedFile.appendMessages(messageExtBatch, this.appendMessageCallback, putMessageContext) mappedFile
  • -> CommitLog#submitFlushRequest 提交刷盘 (异步 / 同步)
  • -> CommitLog#submitReplicaRequest 将消息同步到从节点。可配置备份多少个
  • -> 消息保存完毕

rocketMq 同步消息的原理

  • netty调用方会触发的动作

  • RemotingClient#invokeSync

  • RemotingClient#invokeSyncImpl 发送同步方法的实现

  • this.responseTable.put(opaque, responseFuture) 这个很关键,将opaque 存到响应的 responseTable里边

  • 然后下方 responseFuture.waitResponse(timeoutMillis) 会阻塞当前请求

  • netty被调用方会触发的动作

  • NettyRemotingAbstract 然后我们看此处的方法。

  • RemotingResponseCallback callback = new RemotingResponseCallback() 构建了一个远程

  • 如果请求是同步请求的话,一定会触发 callback.callback(response);

final RemotingResponseCallback callback = new RemotingResponseCallback() {
    @Override
    public void callback(RemotingCommand response) {
        doAfterRpcHooks(remoteAddr, cmd, response);
        if (!cmd.isOnewayRPC()) {
            if (response != null) {
                response.setOpaque(opaque);
                response.markResponseType();
                try {
                    ctx.writeAndFlush(response);
                } catch (Throwable e) {
                    log.error("process request over, but response failed", e);
                    log.error(cmd.toString());
                    log.error(response.toString());
                }
            } else {
            }
        }
    }
};
  • 请观察一下这个的实现。 response.setOpaque(opaque); 想当于将请求的 opaque 塞入到了response里边。

  • 然后将 ctx.writeAndFlush(response); 到调用方

  • 然后回到调用方

  • NettyRemotingAbstract#processMessageReceived 检查到是 RESPONSE_COMMAND 响应的请求

  • responseFuture.putResponse 会设置 responseCommand 并且 countDownLatch.countDown 释放之前阻塞的请求

RocketMq是如何拉取消息的

发送请求:

  • consumer.start();
  • defaultMQPushConsumerImpl.start() ->
  • mQClientFactory.start() ->
  • pullMessageService.start() ->
  • this.pullRequestQueue.take() 关键点是 pullRequestQueue 这个队列,首次会从 rebalanceService.start() 获取到消息->
  • impl.pullMessage(pullRequest) 拉取消息 ->
  • this.pullAPIWrapper.pullKernelImpl 发送拉取消息请求 ->
  • this.mQClientFactory.getMQClientAPIImpl().pullMessage 发送 RequestCode.PULL_MESSAGE 消息 ->

服务器处理请求

  • PullMessageProcessor 专门处理 RequestCode.PULL_MESSAGE 消息 ->
  • PullMessageProcessor#processRequest处理请求 ->
  • ResponseCode.SUCCESS 找到消息的话直接请求客户端 ->
  • ResponseCode.PULL_NOT_FOUND 说明消息消费到最后一条,等待新消息的进入 ->
  • this.brokerController.getPullRequestHoldService().suspendPullRequest(topic, queueId, pullRequest) 将“长轮询pullRequest”对象 交给 长轮询服务 ->
  • pullRequestTable.putIfAbsent(key, mpr) 将请求放入 pullRequestTable ->
  • broker在启动的时候 启动了PullRequestHoldService这个服务 ->
  • PullRequestHoldService 定时扫描 pullRequestTable 关心的数据中是否有新消息->
  • PullRequestHoldService#checkHoldRequest 核心逻辑,循环 pullRequestTable 内容并检查指定的 offset ->
  • PullRequestHoldService#notifyMessageArriving 通知消息到达->
  • notifyMessageArriving 取出 pullRequestTable 指定key的 List<PullRequest> 循环判断 offset是否 大于其中一个 PullRequest的数据, 如果是 ->
  • this.brokerController.getPullMessageProcessor().executeRequestWhenWakeup() 启动一个线程,执行 ->
  • PullMessageProcessor.this.processRequest(channel, request, false) 用 PullRequest 查到最新的消息 ->
  • channel.writeAndFlush 将数据发送回客户端 ->

接受服务端返回的长轮训消息或者是及时响应消息

  • PullCallback 处理回调消息 ->
  • DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest 执行 调用方提供的 callback方法
  • DefaultMQPushConsumerImpl.this.executePullRequestImmediately(pullRequest) 将请求放回 pullRequestQueue 队列中
  • 重复循环拉取操作

RocketMq是如何发送事务消息的

  • TransactionMQProducer 使用特殊的 producer 才能发送事务消息

  • producer.setTransactionListener(transactionListener); 设置事务监听

  • 请自己实现事务监听 TransactionListener executeLocalTransaction 立即执行事务,检查是否提交事务; checkLocalTransaction 检查本地事务状态

  • 设置好准备参数之后,进行发送数据,使用方法 sendMessageInTransaction 发送事务消息

  • sendMessageInTransaction :

    • MessageAccessor.putProperty(msg, MessageConst.PROPERTY_TRANSACTION_PREPARED, "true"); 设置事务的标识
    • this.send(msg) 半消息发送
    • transactionListener.executeLocalTransaction(msg, arg) 发送成功之后执行本地事务 获得本地事务的执行状态
    • this.endTransaction(msg, sendResult, localTransactionState, localException); 给 broker反馈本地事务的状态,如果给返回的不是提交状态 Rocketmq 会有一个 TransactionalMessageCheckService 服务轮训调用 checkLocalTransaction 检查本地事务状态
    • 至此就完结了事务消息的提交
  • this.send(msg) 半消息发送 broker解析 (向broker发送了 带有事务标识的消息)

  • broker 接到消息 SendMessageProcessor#asyncSendMessage

  • 判断如果为事务消息,则调用 this.brokerController.getTransactionalMessageService().prepareMessage(msgInner) 存储数据

  • TransactionalMessageServiceImpl#prepareMessage

  • transactionalMessageBridge.putHalfMessage(messageInner)

  • parseHalfMessageInner(messageInner)

  • msgInner.setTopic(TransactionalMessageUtil.buildHalfTopic()); 修改topic并且落盘。存储半消息

  • 至此,broker半消息存储成功, 后续会由 TransactionalMessageCheckService 来检查状态。 或者当消息发送完毕 executeLocalTransaction 返回提交之后。通知broker提交消息

posted @ 2022-11-24 11:09  云扬四海  阅读(157)  评论(0编辑  收藏  举报