【RocketMQ】消息的拉取

在上一讲中,介绍了消息的存储,生产者向Broker发送消息之后,数据会写入到CommitLog中,这一讲,就来看一下消费者是如何从Broker拉取消息的。

RocketMQ消息的消费以组为单位,有两种消费模式:

广播模式:同一个消息队列可以分配给组内的每个消费者,每条消息可以被组内的消费者进行消费。

集群模式:同一个消费组下,一个消息队列同一时间只能分配给组内的一个消费者,也就是一条消息只能被组内的一个消费者进行消费。

通常使用集群模式的情况比较多,接下来以集群模式(Push模式)为例看一下消息的拉取过程。

消费者启动时处理

消费者在启动的时候主要做了以下几件事情:

  1. Topic订阅处理;
  2. MQClientInstance实例创建;
  3. 加载消费进度存储对象,里面存储了每个消息队列的消费进度;
  4. 从NameServer更新Topic路由信息;
  5. 向Broker进行注册;
  6. 触发负载均衡;

主题订阅处理

RocketMQ消费者以组为单位,启用消费者时,需要设置消费者组名称以及要订阅的Topic信息(需要知道要消费哪个Topic上面的消息):

@RunWith(MockitoJUnitRunner.class)
public class DefaultMQPushConsumerTest {@Mockprivate MQClientAPIImpl mQClientAPIImpl;private static DefaultMQPushConsumer pushConsumer;@Beforepublic void init() throws Exception {// ...// 消费者组名称String consumerGroup = "FooBarGroup";// 实例化DefaultMQPushConsumerpushConsumer = new DefaultMQPushConsumer(consumerGroup);pushConsumer.setNamesrvAddr("127.0.0.1:9876");// ...// 设置订阅的主题pushConsumer.subscribe("FooBar", "*");// 启动消费者pushConsumer.start();}
}

所以消费者启动的时候,首先会获取订阅的Topic信息,由于一个消费者可以订阅多个Topic,所以消费者使用一个Map存储订阅的Topic信息,KEY为Topic名称,VALUE为对应的表达式,之后会遍历每一个订阅的Topic,然后将其封装为SubscriptionData对象,并加入到负载均衡对象RebalanceImpl中,等待进行负载均衡。

MQClientInstance实例创建

MQClientInstance中有以下几个关键信息:

  • 消息拉取服务:对应实现类为PullMessageService,是用来从Broker拉取消息的服务;
  • 负载均衡服务:对应的实现类为RebalanceService,是用来进行负载均衡,为每个消费者分配对应的消费队列;
  • 消费者列表(consumerTable):记录该实例上的所有消费者信息,key为消费者组名称,value为消费者对应的MQConsumerInner对象,每一个消费者启动的时候会向这里注册,将自己加入到consumerTable中;

需要注意MQClientInstance实例是以clientId为单位创建的,相同的clientId共用一个MQClientInstance实例,clientId由以下信息进行拼装:
(1)服务器的IP;
(2)实例名称(instanceName);
(3)单元名称(unitName)(不为空的时候才拼接);
最终拼接的clientId字符串为:服务器IP + @ + 实例名称 + @ + 单元名称
所以在同一个服务器上,如果实例名称和单元名称也相同的话,所有的消费者会共同使用一个MQClientInstance实例。

MQClientInstance启动的时候会把消息拉取服务和负载均衡服务也启动(启动对应的线程)。

获取Topic路由信息

前面已经得知了当前消费者订阅的Topic信息,接下来需要知道这些Topic的分布情况,也就是分布在哪些Broker上,Topic的分布信息可以从NameServer中获取到,因为Broker会向NameServer进行注册,上报自己负责的Topic信息,所以这一步消费者向NameServer发送请求,从NameServer中拉取最新的Topic的路由信息缓存在本地。

加载消费进度

消费者在进行消费的时候,需要知道应该从哪个位置开始拉取消息,OffsetStore类中记录这些数据,不同的模式对应的实现类不同:

  • 集群模式:消息的消费进度保存在Broker中,由Broker记录每个消费队列的消费进度,对应实现类为RemoteBrokerOffsetStore
  • 广播模式:消息的消费进度保存在消费者端,对应实现类为LocalFileOffsetStore

这里关注集群模式,在集群模式下,加载消费进度时,会进入RemoteBrokerOffsetStore的load方法,load方法是从本地加载文件读取消费进度,因为集群模式下需要从Broker获取,所以load方法什么也没干,在负载均衡分配了消息队列,进行消息拉取的时候再向Broker发送请求获取消费进度。

向Broker进行注册

由于消费者增加或者减少会影响消息队列的分配,所以Broker需要感知消费者的上下线情况,消费者在启动时会向所有的Broker发送心跳包进行注册,通知Broker消费者上线。

Broker收到消费者发送的心跳包之后,会从请求中解析相关信息,将该消费者注册到Broker维护的消费者列表consumerTable中,其中KEY为消费者组名称,Value为该消费组的详细信息(ConsumerGroupInfo对象),里面记录了该消费组下所有消费者的Channel信息。

触发负载均衡

启动最后一步,会立即触发一次负载均衡,为消费者分配消息队列。

负载均衡

负载均衡是通过消费者启动时创建的MQClientInstance实例实现的(doRebalance方法),它的处理逻辑如下:

  1. MQClientInstance中有一个消费者列表consumerTable,存放了该实例上注册的所有消费者对象,Key为组名称,Value为消费者,所以会遍历所有的消费者,对该实例上注册的每一个消费者进行负载均衡;

  2. 对于每一个消费者,需要获取其订阅的所有Topic信息,然后再对每一个Topic进行负载均衡,前面可知消费者订阅的Topic信息被封装为了SubscriptionData对象,所以这里获取到所有的SubscriptionData对象进行遍历,开始为每一个消费者分配消息队列;

分配消息队列

这里我们关注集群模式下的分配,它的处理逻辑如下:

  1. 根据Topic获取该Topic下的所有消费队列(MessageQueue对象);

消费者在启动时向NameServer发送请求获取Topic的路由信息,从中解析中每个主题对应的消息队列,放入负载均衡对象的topicSubscribeInfoTable变量中,所以这一步直接从topicSubscribeInfoTable中获取主题对应的消息队列即可。

  1. 根据主题信息和消费者组名称,查找订阅了该主题的所有消费者的ID:
    (1)根据主题选取Broker:从NameServer中拉取的主题路信息中可以找到每个主题分布在哪些Broker上,从中随机选取一个Broker;
    (2)向Broker发送请求:根据上一步获取到的Broker,向其发送请求,查找订阅了该主题的所有消费者的ID(消费者会向Broker注册,所以可以通过Broker查找订阅了某个Topic的消费者);

  2. 如果主题对应的消息队列集合和获取到的消费者ID都不为空,对消息队列集合和消费ID集合进行排序;

  3. 获取分配策略,根据具体的分配策略,为当前的消费者分配对应的消费队列,RocketMQ默认提供了以下几种分配策略:

    • AllocateMessageQueueAveragely:平均分配策略,根据消息队列的数量和消费者的个数计算每个消费者分配的队列个数。

    • AllocateMessageQueueAveragelyByCircle:平均轮询分配策略,将消息队列逐个分发给每个消费者。

    • AllocateMessageQueueConsistentHash:根据一致性 hash进行分配。

    • AllocateMessageQueueByConfig:根据配置,为每一个消费者配置固定的消息队列 。

    • AllocateMessageQueueByMachineRoom:分配指定机房下的消息队列给消费者。

    • AllocateMachineRoomNearby:优先分配给同机房的消费者。

  4. 根据最新分配的消息队列,更新当前消费者负责的消息处理队列;

更新消息处理队列

每个消息队列(MessageQueue)对应一个处理队列(ProcessQueue),后续使用这个ProcessQueue记录的信息进行消息拉取:

分配给当前消费者的所有消息队列,由一个Map存储(processQueueTable),KEY为消息队列,value为对应的处理队列:

由于负载均衡之后,消费者负责的消息队列可能发生变化,所以这里需要更新当前消费者负责的消息队列,它主要是拿负载均衡后重新分配给当前消费的消息队列集合与上一次记录的分配信息做对比,有以下两种情况:

(1)某个消息队列之前分配给了当前消费者,但是这次没有,说明此队列不再由当前消费者消负责,需要进行删除,此时将该消息队列对应的处理队列中的dropped状态置为true即可;
(2)某个消费者之前未分配给当前消费者,但是本次负载均衡之后分配给了当前消费者,需要进行新增,会新建一个处理队列(ProcessQueue)加入到processQueueTable中;

对于情况2,由于是新增分配的消息队列,消费者还需要知道从哪个位置开始拉取消息,所以需要通过OffsetStore来获取存储的消费进度,也就是上次消费到哪条消息了,然后判断本次从哪条消息开始拉取。前面在消费者启动的提到集群模式下对应的实现类为RemoteBrokerOffsetStore,再进入到这一步的时候,才会向Broker发送请求,获取消息队列的消费进度,并更新到offsetTable中。

从Broker获取消费进度之后,有以下几种拉取策略:
(1)CONSUME_FROM_LAST_OFFSET(上次消费位置开始拉取):从OffsetStore获取消息队列对应的消费进度值lastOffset,判断是否大于等于0,如果大于0则返回lastOffset的值,从这个位置继续拉取;
(2)CONSUME_FROM_FIRST_OFFSET(第一个位置开始拉取):从OffsetStore获取消息队列对应的消费进度值lastOffset,如果大于等于0,依旧从这个位置继续拉取,否则才从第一条消息拉取,此时返回值为0;
(3)CONSUME_FROM_TIMESTAMP(根据时间戳拉取):从OffsetStore获取消息队列对应的消费进度值lastOffset,如果大于等于0,依旧从这个位置继续拉取,否则在不是重试TOPIC的情况下,根据消费者的启动时间查找应该从什么位置开始消费;

nextOffset拉取偏移量的值确定之后,会将ProcessQueue加入到processQueueTable中,并构建对应的消息拉取请求PullRequest,并设置以下信息:

  • consumerGroup:消费者组名称;
  • nextOffset:从哪条消息开始拉取,设置的是上面计算的消息拉取偏移量nextOffset的值;
  • MessageQueue:消息队列,从哪个消息队列上面消费消息;
  • ProcessQueue:处理队列,消息队列关联的处理队列;

PullRequest构建完毕之后会将其加入到消息拉取服务中的一个阻塞队列中,等待消息拉取服务进行处理。

消息拉取

消费者发送拉取请求

消息拉取服务中,使用了一个阻塞队列,阻塞队列中存放的是消息拉取请求PullRequest对象,如果有消息拉取请求到来,就会从阻塞队列中取出对应的请求进行处理,从Broker拉取消息,拉取消息的处理逻辑如下:

  1. 从拉取请求PullRequest中获取对应的处理队列ProcessQueue,先判断是否置为Dropped删除状态,如果处于删除状态不进行处理;
  2. 从处理队列中获取缓存的消息的数量及大小进行验证判断是否超过了设定的值,因为处理队列中之前可能已经拉取了消息还未处理完毕,为了不让消息堆积需要先处理之前的消息,所以会延迟50毫秒后重新加入到拉取请求队列中处理;
  3. 判断是否是顺序消费,这里先不讨论顺序消费,如果是非顺序消费,判断processQueue中队列最大偏移量和最小偏移量的间距是否超过ConsumeConcurrentlyMaxSpan的值,如果超过需要进行流量控制,延迟50毫秒后重新加入队列中进行处理;
  4. 向Broker发送拉取消息请求,从Broker拉取消息:
    (1)ProcessQueue关联了一个消息队列MessageQueue对象,消息队列对象中有其所在的Broker名称,根据名称再查找该Broker的详细信息;
    (2)根据第(1)步的查找结果,构建消息拉取请求,在请求中设置本次要拉取消息的Topic名称、消息队列ID等信息,然后向Broker发送请求;
  5. 消费者处理拉取请求返回结果,上一步向Broker发送请求的时候可以同步发送也可以异步发送请求,对于异步发送请求当请求返回成功之后,会有一个回调函数,在回调函数中处理消息拉取结果。

Broker处理消息拉取请求

ConsumeQueue
RocketMQ在消息存储的时候将消息顺序写入CommitLog文件,如果想根据Topic对消息进行查找,需要扫描所有CommitLog文件,这种方式性能低下,所以RocketMQ又设计了ConsumeQueue存储消息的逻辑索引,在RocketMQ的存储文件目录下,有一个consumequeue文件夹,里面又按Topic分组,每个Topic一个文件夹,Topic文件夹内是该Topic的所有消息队列,以消息队列ID命名文件夹,每个消息队列都有自己对应的ConsumeQueue文件:

ConsumeQueue中存储的每条数据大小是固定的,总共20个字节:

  • 消息在CommitLog文件的偏移量,占用8个字节;
  • 消息大小,占用4个字节;
  • 消息Tag的hashcode值,用于tag过滤,占用8个字节;

Broker在收到消费发送的拉取消息请求后,会根据拉取请求中的Topic名称和消息队列ID(queueId)查找对应的消费信息ConsumeQueue对象:
Broker中的consumeQueueTable中存储了每个Topic对应的消费队列信息,Key为Topic名称,Value为Topic对应的消费队列信息,它又是一个MAP,其中Key为消息队列ID(queueId),value为该消息队列的消费消费信息(ConsumeQueue对象)。

在获取到息ConsumeQueue之后,从中可以获取其中记录的最小偏移量minOffset和最大偏移量maxOffset,然后与拉取请求中携带的消息偏移量offset的值对比进行合法校验,校验通过才可以查找消息,对于消息查找结果大概有如下几种状态:

nextOffsetCorrection方法:用于校正消费者的拉取偏移量,不过需要注意,当前Broker是主节点或者开启了OffsetCheckInSlave校验时,才会对拉取偏移量进行纠正,所以以下几种状态中如果调用了此方法进行校正,前提是满足此条件。

  1. NO_MESSAGE_IN_QUEUE:如果CommitLog中的最大偏移量maxOffset值为0,说明当前消息队列中还没有消息,返回NO_MESSAGE_IN_QUEUE状态;
  2. OFFSET_TOO_SMALL:如果待拉取偏移量offset的值小于CommitLog文件的最小偏移量minOffset,说明拉取进度值过小,调用nextOffsetCorrection校正下一次的拉取偏移量为CommitLog文件的最小偏移量(需要满足校正的条件),并将这个偏移量放入nextBeginOffset变量;
  3. OFFSET_OVERFLOW_ONE:如果待拉取偏移量offset等于CommitLog文件的最大偏移量maxOffset,依旧调用nextOffsetCorrection方法进行校正(需要满足校正的条件),只不过校正的时候使用的还是offset的值,可以理解为这种情况什么也没干。
  4. OFFSET_OVERFLOW_BADLY:如果待拉取偏移量offset大于CommitLog文件最大偏移量maxOffset,说明拉取偏移量越界,此时有以下两种情况:
    • 如果最小偏移minOffset量为0,调用nextOffsetCorrection方法校正下一次拉取偏移量为minOffset的值(需要满足校正的条件),也就是告诉消费者,下次从偏移量为0的位置开始拉取消息;
    • 如果最小偏移量minOffset不为0,调用nextOffsetCorrection方法校正下一次拉取偏移量为maxOffset的值(需要满足校正的条件),将下一次拉取偏移量的值设置为最大偏移量;
  5. NO_MATCHED_LOGIC_QUEUE:如果根据主题未找到消息队列,返回此状态;
  6. FOUND:待拉取消息偏移量offset的值介于最大最小偏移量之间,此时可以正常查找消息;

需要注意以上是消息查找的结果状态,Broker并没有使用这个状态直接返回给消费者,而是又做了一次处理。

经过以上步骤后,除了查找到的消息内容,Broker还会在消息返回结果中设置以下信息:

  1. 查找结果状态;
  2. 下一次拉取的偏移量,也就是nextBeginOffset变量的值;
  3. CommitLog文件的最小偏移量minOffset和最大偏移量maxOffset

消费者对拉取结果的处理

消费者收到Broker返回的响应后,对响应结果进行处理:

  1. FOUND:消息拉取请求成功,此时从响应中获取Broker返回的下一次拉取偏移量的值,更新到拉取请求中,然后进行以下判断:
    • 如果拉取到的消息内容为空,将拉取请求放入到阻塞队列中再进行一次拉取;
    • 如果拉取到的消息内容不为空,将消息提交到ConsumeMessageService中进行消费(异步处理),然后判断拉取间隔的值是否大于0,如果大于0,会延迟一段时间进行下一次拉取,如果拉取间隔小于0表示需要立刻进行下一次拉取,此时将拉取请求加入阻塞队列中进行下一次拉取。
  2. NO_MATCHED_MSG:没有匹配的消息,使用Broker返回的下一次拉取偏移量的值作为新的拉取消息偏移量,然后将拉取请求加入阻塞队列中立刻进行下一次进行拉取。
  3. OFFSET_ILLEGAL:拉取偏移量不合法,此时使用Broker返回的下一次拉取偏移量的值,更新到消费者记录的消息拉取偏移量中(offsetStore),并持久化保存,然后将当前的拉取请求中的处理队列状态置为dorp并删除处理队列,等待下一次重新构建拉取请求进行处理。

RocketMQ消息拉取相关源码可参考:【RocketMQ】【源码】消息的拉取

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/78487.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Element Plus table formatter函数返回html内容

查看 Element Plus table formatter 支持返回 类型为string 和 VNode对象; 若依全局直接用h函数,无需引用 下面普通基本用法:在Element Plus中,你可以使用自定义的formatter函数来返回VNode对象,从而实现更灵活的自定…

Navicat连接mysql8.0:提示无法加载身份验证插件“caching_sha2_password”

Navicat连接mysql时,提示:Unable to load authentication plugin ‘caching_sha2_password‘. 原因:mysql 8.0 默认使用 caching_sha2_password 身份验证机制。 D:\MySQL8.0\install\bin>mysql -uroot -p123456789 #登录 mysql: [War…

《动手学深度学习 Pytorch版》 4.10 实战Kaggle比赛:预测比赛

4.10.1 下载和缓存数据集 import hashlib import os import tarfile import zipfile import requests#save DATA_HUB dict() DATA_URL http://d2l-data.s3-accelerate.amazonaws.com/def download(name, cache_diros.path.join(.., data)): #save"""下载一个…

Pyramid Scene Parsing Network–CVPR,2017论文解读及其pytorch代码

文章目录 Pyramid Scene Parsing Network--CVPR,2017一、背景介绍二、网络结构和优化方法三、实验结果 Pyramid Scene Parsing Network–CVPR,2017 Github代码链接 一、背景介绍 现阶段随着数据集制作精细化、标签种类变多、具有语义相似标签等导致出…

DataX实现Mysql与ElasticSearch(ES)数据同步

文章目录 一、Linux环境要求二、准备工作2.1 Linux安装jdk2.2 linux安装python2.3 下载DataX: 三、DataX压缩包导入,解压缩四、编写同步Job五、执行Job六、定时更新6.1 创建定时任务6.2 提交定时任务6.3 查看定时任务 七、增量更新思路 一、Linux环境要求…

实时显示当前文件夹下的文件大小,shell脚本实现

图片来源于网络,如果侵权请联系博主删除! 需求: 写一个shell终端命令,实时显示当前文件夹下的文件大小 实现: 您可以使用以下的Shell脚本命令来实时显示当前文件夹下的文件大小: while true; docleardu …

2023-亲测有效-git clone失败怎么办?用代理?加git?

git 克隆不下来,超时 用以下格式: git clone https://ghproxy.com/https://github.com/Tencent/ncnn.git 你的网站前面加上 https://ghproxy.com/ 刷的一下就下完了!!

投资理财基础知识分享(三)

大家好,我是财富智星,今天跟继续跟大家分享投资理财基础知识,买理财,何时买,何时卖的问题。 一、何时买? 在投资中,决定何时买入资产对于理财新手来说并不是一个应该花费过多精力的问题&#xf…

音频驱动嘴型的视频数字人虚拟主播工具motionface replay使用教程

音频驱动嘴型的视频数字人虚拟主播工具motionface replay使用教程 1:系统要求 软件运行支持32位/64位window 10/11系统,内存最低要求> 8Gb.无其他硬性要求。 1:下载安装 打开百度网盘链接下载: 链接:百度网盘 请输入…

Transformers-Bert家族系列算法汇总

🤗 Transformers 提供 API 和工具,可轻松下载和训练最先进的预训练模型。使用预训练模型可以降低计算成本、碳足迹,并节省从头开始训练模型所需的时间和资源。这些模型支持不同形式的常见任务,例如: 📝 自…

Vue3统一导出局部组件和全局组件

局部组件统一导出 components新增ComponentA.vue、ComponentB.vue两个组件 新增index.js进行组件统一导入 import ComponentA from ./ComponentA.vue import ComponentB from ./ComponentB.vueexport {ComponentA,ComponentB }使用 <template><ComponentA /><…

最新模块化设计小程序系统源码完整版:开源可二开,支持DIY

随着互联网的快速发展&#xff0c;小程序已成为各行各业开展业务的重要工具。而模块化设计小程序系统源码完整版则是一种高效、灵活、易维护的解决方案。 分享一个最新的模块化设计小程序系统源码完整版&#xff0c;源码开源可二开&#xff0c;支持自由DIY设计&#xff0c;含完…

怎么裁剪图片?总结了下面几个方法

怎么裁剪图片&#xff1f;在日常的生活中&#xff0c;图片已经成为了我们不可或缺的一部分。或许你正在整理自己的相册时&#xff0c;或者我们需要向互联网上发布一些图片的时候&#xff0c;总之我们随时都可能会遇到一张需要进行裁剪的图片。比如说&#xff0c;一些图片上存在…

Unity 课时 4 : No.4 模拟面试题

课时 4 : No.4 模拟面试题 C# 1. 请说明字符串中 string str null string str “” string str string.Empty 三者的区别 第一个未作初始化没有值, 第二个为空字符串, 答案&#xff1a; str null 在堆中没有分配内存地址 str "" 和 string.Empty 一样都是…

新加坡亲子游景点推荐

长久以来&#xff0c;新加坡和马来西亚都是中国游客非常青睐的出境游目的地之一&#xff0c;极近的距离、便捷的交通、亲切的语言和文化环境使得这里的旅游体验备受赞誉。如果你准备带孩子去新加坡和马来西亚观光游&#xff0c;可以让你们享受到更多样的亲子旅行体验噢~ Wild W…

设计模式汇总

设计模式本质上是某类特定问题的代码设计解决方案&#xff0c;实际上是一套某类问题的代码设计经验总结。&#xff08;前辈总结的解决某类问题的切实可行的套路&#xff09; 问题 1、为什么要使用设计模式&#xff1f; 答&#xff1a;1、 提高代码复用率&#xff0c;降低开发成…

【网络通信 -- WebRTC】FlexFec 基本知识点总结概述

【网络通信 -- WebRTC】FlexFec 基本知识点总结概述 【1】FlexFec 的保护方案 假设存在一组源数据包(D L)&#xff0c;其序列号从 1 开始运行到 D L 一维非交错行 FEC(1-D Non-interleaved Row FEC) : 一种连续的源数据包进行保护的方案&#xff0c;可用于恢复按行分组的源…

Linux内存管理--smaps内存

一、内存的两个概念 了解smaps内存之前要先搞清楚Linux内存管理中的虚拟内存&#xff08;Virtual Memory&#xff09;和驻留内存&#xff08;Resident Memory&#xff09;两个概念。 1、虚拟内存 首先需要强调的是虚拟内存不同于物理内存&#xff0c;虽然两者都包含内存字眼…

微信会员卡开发流程

功能需求&#xff1a; 通过微信第三方平台创建的模板小程序&#xff0c;想要实现用户在小程序支付一定金额后领取会员卡&#xff0c;领取会员卡后可给用户下发一定数量的优惠券&#xff0c;并且实现用户在小程序消费享受商品折扣。 开发流程&#xff1a; 一、了解微信的3个平…

Java高级: 反射

目录 反射反射概述反射获取类的字节码反射获取类的构造器反射获取构造器的作用反射获取成员变量&使用反射获取成员方法反射获取成员方法的作用 反射的应用案例 接下来我们学习的反射、动态代理、注解等知识点&#xff0c;在以后开发中极少用到&#xff0c;这些技术都是以后…