前言
本章基于rocketmq5.1.1分析pop消费 特性,具体特性的描述和用途参考[RIP-19]。
pop消费的特点在于:
- broker负责rebalance,为每个consumer分配queue,支持队列非独占(一个队列分配给组内多个consumer消费);
- broker负责管理消费进度,包括拉消息和提交offset;
案例
consumer侧,使用setClientRebalance=false,关闭客户端rebalance。
java
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
consumer.setNamesrvAddr("localhost:9876");
consumer.subscribe(TOPIC, "*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.setClientRebalance(false); // 【关闭client rebalance】
consumer.start();
}
broker侧,可通过两种方式开启pop消费。
1、topic+consumerGroup纬度
如通过mqadmin setConsumeMode命令,设置某个broker某个topic在某个consumerGroup下开启pop消费。
注:也可以通过-c选项代替-b选项,设置某个cluster,本质是循环cluster下所有broker。
css
mqadmin setConsumeMode -b brokerAddr -t topic -g consumer_group -m POP -n 8
2、broker纬度
配置defaultMessageRequestMode=POP(默认PULL,即老逻辑队列独占),针对所有topic和consumerGroup开启pop消费,优先级低于第一种。
ini
defaultMessageRequestMode=POP
consumer
consumer侧消费流程还是大致如下,主要变化在于Rebalance 、Pull 、Consume 、重试。
主流程可参考4.x的逻辑:RocketMQ4源码(三)普通消息消费。
broker
rebalance:原来pull模式默认平均分配策略,pop模式如何分配队列?
pull/consume:原来pull模式消费进度由每个consumer管理,如果队列非独占,会造成消费进度混乱。pop模式如何管理消费进度?
重试:原来pull模式通过consumer主动订阅发布%RETRY%{consumerGroup}消息实现消费重试,pop模式使用changeInvisibleTime实现重试有什么区别?
注:
1)详细的消费逻辑忽略
包括如何通过逻辑offset定位commitlog,拉消息长轮询处理等。
2)忽略各种5.x其他新特性
5.x很多新特性,代码复杂度极高,可以忽略。
比如代码里看到很多CompletableFuture是RIP-57对于多级存储的实现而引入异步api,实际底层还是同步。
Rebalance
consumer启用broker rebalance
开启pop消费的前提是,consumer需要启用broker rebalance,由broker执行queue分配。
RebalanceImpl#doRebalance:客户端rebalance方法入口,执行broker rebalance需要满足两个条件。
RebalancePushImpl#clientRebalance:满足下面条件开启client rebalance老逻辑。
相反,第一,满足下面4个条件,开启broker rebalance逻辑:
- 使用push api(lite pull和pull都不支持);
- 关闭clientRebalance;
- 非顺序消费(并发消费);
- 非广播消费(集群消费);
RebalanceImpl#tryQueryAssignment:第二,broker能够正常执行QUERY_ASSIGNMENT。
consumer调用某个有对应topic的broker的QUERY_ASSIGNMENT api,如果成功调用,则允许执行broker rebalance。
RebalanceImpl#getRebalanceResultFromBroker:
启用broker rebalance后,consumer从broker获取分配的queue,更新分配给自己的ProcessQueue。
broker分配队列
开启POP消费
默认情况下,即使客户端开启broker rebalance,也仅仅代表由broker分配queue,并没有开启pop消费。
broker端开启pop消费的方式,见前言案例。
SetMessageRequestModeRequestBody是broker端针对topic+consumerGroup的消费方式。
mode=PULL(默认),直接走分配策略(如默认平均分配)为每个客户端分配queue,即队列独占;
mode=POP,开启pop消费,支持共享队列;
Pull or Pop
QUERY_ASSIGNMENT 的第一步需要确定MessageRequestMode,即Pull还是Pop。
- 优先走topic+consumerGroup纬度的配置;
- 默认走broker纬度的配置;
- 对于重试topic只能走pull模式老逻辑;
注意:这里重试topic是pull模式的%RETRY%{consumerGroup},后面提到的pop重试topic不会走这里。
针对topic+consumerGroup纬度的配置,都存放在MessageRequestModeManager中。
注:内存数据会持久化到messageRequestMode.json文件中。
分配queue
QueryAssignmentProcessor#doLoadBalance:
和客户端rebalance一样,先要得到topic下所有queue和消费组内所有成员clientId。
QueryAssignmentProcessor#doLoadBalance:
区分pull模式还是pop模式。
如果是pull模式,直接执行原始AllocateMessageQueueStrategy分配,比如默认平均分配策略。
QueryAssignmentProcessor#allocate4Pop:
pop模式分配,有三种情况。
case1:
默认情况下 ,popShareQueueNum=-1,所有consumer实例会全量分配queue。
如果popShareQueueNum大于等于consumer实例数量-1,也会全量分配queue。
注意,这里queueId设置为-1,最后MessageQueue去重后,数量为topic对应master broker数量。
相当于客户端不再关心实际broker&topic下有多少个队列。
case2:
如果popShareQueueNum小于consumer实例数量-1,且consumer数量小于等于queue数量。
执行popShareQueueNum+1次分配策略,去重后加入结果集。
比如使用平均分配策略,8个queue,3个client,popShareQueueNum=1。
index=0的client分配q0-q5,index=1的client分配q3-q7,index=2的client分配q6-q7和q0-q2。
case3:
不满足上述条件,即popShareQueueNum小于consumer实例数量-1,且consumer数量大于queue数量。
QueryAssignmentProcessor#allocate:每个consumer实例分配1个queue。
所以不合理的popShareQueueNum+consumer数量+queue数量仍然会导致队列独占,比如8个queue,16个consumer,popShareQueueNum小于15。
最终分配结果转换为MessageQueueAssignment返回客户端。
consumer更新ProcessQueue
RebalanceImpl#updateMessageQueueAssignment:
如果broker开启pop消费,所有MessageQueueAssignment都是POP模式。
重试topic订阅
pull模式,每个consumerGroup会默认订阅重试topic,即%RETRY%{consumerGroup},存在对应ProcessQueue。
pop模式,在topic+consumerGroup纬度还会存在n个订阅,即 %RETRY%{consumerGroup}_{topic} ,不存在ProcessQueue。
在更新ProcessQueue的过程中,会对pop重试topic订阅关系做处理。
RebalanceImpl#updateMessageQueueAssignment:
如果当前是push模式,需要订阅pop重试topic;
而如果当前是pop模式,则需要【取消】订阅pop重试topic;
此外pop重试topic不存在对应ProcessQueue。
pop模式既然不关注重试topic,那是怎么工作的呢?
分配PopProcessQueue&提交PopRequest
对于pull模式,每个MessageQueue对应一个ProcessQueue;
对于pop模式,每个MessageQueue对应一个PopProcessQueue。
PopProcessQueue不同于原来的ProcessQueue,不再包含任何offset相关属性。
RebalanceImpl#updateMessageQueueAssignment:
对于移除分配给自己的queue,仍然是标记drop并从RebalanceImpl的popProcessQueueTable中移除,不具体看了。
对于新分配给自己的queue,构造PopRequest提交到PullMessageService线程。
RebalancePushImpl#getConsumeInitMode:
和原来PullRequest的区别是,PopRequest没有指定队列逻辑offset,仅仅指定了consumeInitMode。
ConsumeInitMode分为两种:
- 默认MAX;
- 可配置ConsumeFromWhere=CONSUME_FROM_FIRST_OFFSET,修改为MIN;
Pull
consumer发送pop消息请求
PullMessageService#run:
consumer的PullMessage线程消费PopRequest。
DefaultMQPushConsumerImpl#popMessage:
consumer经过一系列校验后,如流控,提交请求。
PullAPIWrapper#popAsync:
pop消息请求仅支持发送给master broker。如果master不存在,延迟3s再提交PopRequest。
注:上一章提到(4.x的HA),普通pull消费是可以走slave的,见PullAPIWrapper#pullKernelImpl。
PullAPIWrapper#popAsync:相较于普通pull消息,pop消息请求的区别是:
- 没有commitOffset 参数,不支持由consumer顺便提交offset;
- 没有queueOffset 参数,拉取逻辑offset,只有一个initMode=MAX或MIN,默认MAX;
- 多了invisibleTime参数,默认60s;
broker回复pop消息响应
拉消息主流程
在rebalance阶段知道,pop消费的重试topic是不存在ProcessQueue概念的。
也就是说consumer针对pop重试topic不会主动请求broker来拉消息。
PopMessageProcessor#processRequest:
其实broker在pop消息时,broker根据一定概率回复重试消息,这个概率是五分之一。
PopMessageProcessor#processRequest:
默认情况下,popShareQueueNum=-1,导致queueId为-1。
从随机下标开始,循环队列数量次数,pop消息。
如果根据popShareQueueNum+cid+queue数量分配到实际队列,pop指定队列消息即可。
PopMessageProcessor#processRequest:
最后,如果没拉过重试队列消息(五分之四概率),且普通队列消息没拉满32条(客户端默认指定),才会从重试队列拉消息。
PopMessageProcessor#processRequest:
拉取到消息,正常回复consumer,如果restNum>0,代表还有消息可以拉取,唤醒其他长轮询请求;
未拉取到消息,开启长轮询,挂起当前请求。
拉消息细节
PopMessageProcessor#popMsgFromQueue:
pop虽然让consumer侧队列共享,但是在broker侧拉消息请求还是得保证队列独占。
topic+consumerGroup+queueId有互斥锁保护。(细节不深入)
一个queue,同时组内多个consumer发来pop请求,只有一个能成功拉取。
假设场景:
客户端c1的一次pop请求,所有queue都没成功获取锁,那么最终会导致进入长轮询。
其他客户端的pop请求,成功获取锁,如果最后有消息没拉完,即restNum>0,那么将唤醒c1。
这个就有点像JDK的AQS的共享模式,AbstractQueuedSynchronizer#acquireShared。
接下来,单线程处理一个队列的拉消息逻辑。
PopMessageProcessor#popMsgFromQueue:
首先查询pop消费进度(后面再看),然后判断是否已经拉满32条,如果拉满直接返回。
PopMessageProcessor#popMsgFromQueue:
根据getPopOffset得到的消费进度拉消息,得到GetMessageResult,总体逻辑大致了解(按4.x理解)。
PopMessageProcessor#popMsgFromQueue:拉到消息
- 记录checkpoint
- 构建startOffsetInfo,拼接上当前queue和这批消息的逻辑offset开始位置,用于响应客户端
- 构建msgOffsetInfo,拼接上当前queue和每条消息对应逻辑offset,用于响应客户端
CheckPoint
PopCheckPoint 记录一次pop请求针对某个queue成功拉到消息的情况。
PopMessageProcessor#appendCheckPoint:
PopBufferMergeService#addCkJustOffset:
- 存储checkpoint;
- 内存缓存lockKey(topic+consumerGroup+queueId)对应checkpoint列表;
- 内存缓存checkpoint唯一键(topic+consumerGroup+queueId+起始消费进度+pop请求时间戳+broker名)对应checkpoint;
PopBufferMergeService#putCkToStore:
本质上checkpoint的存储方式,还是发一个消息到一个系统topic=rmq_sys_REVIVE_LOG_{clusterName} ,tag=ck。
checkpoint消息包含几个特殊的属性:
- deliverTimeMs:pop请求时间+invisibleTime(60s)-1;
- checkpoint唯一键
收到消息(Public线程)
DefaultMQPushConsumerImpl#popMessage:
consumer的io线程收到消息后,交给public线程处理。
DefaultMQPushConsumerImpl#popMessage$PopCallback:和4.x逻辑差不多
- 反序列化解析为PopResult;
- 投递一个ConsumeRequest到consume线程池。和pull模式一致,每个消费组consumer实例一个线程池,默认20线程+LinkedBlockingQueue;
- 立即发起下一次长轮询,即继续提交PopRequest到PullMessage线程池;
ConsumeMessagePopConcurrentlyService.ConsumeRequest:
- msgs:默认批处理大小为1,就一条消息;
- processQueue:Pop处理队列;
- messageQueue:分配队列;
- popTime:broker收到pop请求的时间戳;
- invisibleTime:consumer发送pop请求时设置,默认60s;
Consume
consumer
ConsumeRequest#run:执行用户Listener业务代码,如果pop未超时,处理消费结果。
ConsumeRequest#isPopTimeout:
如果broker收到pop请求(popTime )后超过60s(invisibleTime)还未执行用户消费逻辑,
则本次pop请求认为超时,不会执行用户消费逻辑,也不会处理消费结果。
ConsumeMessagePopConcurrentlyService#processConsumeResult:
pop消费成功,循环处理每一条被ack的消息。
回顾传统pull模式,还需要移除ProcessQueue中缓存的message,而PopProcessQueue几乎只是个pojo。
DefaultMQPushConsumerImpl#ackAsync:
consumer异步ack(ACK_MESSAGE)一条消息的offset,和拉消息一样也仅支持发送请求给master。
回顾传统pull模式,这里是提交offset到本地缓存,pull请求或后台定时向broker提交offset(UPDATE_CONSUMER_OFFSET)。
broker
AckMessageProcessor#processRequest:
broker收到ack后,发送一个AckMsg 到topic=rmq_sys_REVIVE_LOG_{clusterName} ,tag=ack。
queueId为之前pull消息阶段存储的checkpoint消息(tag=ck)的queueId(rqId)。
即checkpoint和ack消息发送到同一个队列,前者tag是ck,后者tag是ack。
broker消费进度管理
PopBufferMergeService扫描
回顾Pull消息流程,在broker侧除了回复consumer之外做了三件事情。
PopBufferMergeService#addCkJustOffset:
- 存储checkpoint,即发送ck消息到topic=rmq_sys_REVIVE_LOG_{clusterName};
- 内存缓存lockKey(topic+consumerGroup+queueId)对应checkpoint列表,即PopBufferMergeService#commitOffsets;
- 内存缓存checkpoint唯一键(topic+consumerGroup+queueId+起始消费进度+pop请求时间戳+broker名)对应checkpoint,即PopBufferMergeService#buffer;
PopBufferMergeService 同时也是个后台线程ServiceThread 负责管理offset。
PopBufferMergeService 每隔5ms扫描内存中的buffer 和commitOffsets进行offset提交。
PopBufferMergeService#scan:
先扫描buffer,后扫描commitOffsets。
buffer扫描
PopBufferMergeService#scan:
对于checkpoint存储成功,buffer可以移除。
PopBufferMergeService#scan:
实际在pull消息时,ck消息允许发送不成功 ,此时在扫描buffer时重新尝试存储checkpoint。
commitOffsets扫描
PopBufferMergeService#scanCommitOffset:
消费commitOffsets内存队列,在checkpoint存储成功的情况下,尝试提交offset。
这意味着,在consumer从broker拉消息成功之后,offset就可能会被提交。
类似于自动提交,至于怎么保证at least once,依靠后续补偿手段。
注:为什么在拉消息后自动提交,而不是在consumer ack时提交,
PopBufferMergeService#commitOffset:
获取topic+consumerGroup+queueId的互斥锁之后(与同队列拉消息pop请求互斥),提交offset到内存offsetTable。
查询offset
PopMessageProcessor#getPopOffset:broker在pop拉消息时查询offset的逻辑。
1)先查内存offsetTable消费进度,即底层消费进度;
2)如果底层消费进度不存在,根据initMode设置初始消费进度,默认MAX,即最大逻辑offset,可选MIN,即最小逻辑offset;
3)最后查询PopBufferMergeService中的commitOffsets进度,取最大值作为本次拉取消息的offset;
PopBufferMergeService#getLatestOffset:
PopBufferMergeService取commitOffsets中最后一个checkpoint的下一个offset返回。
enablePopBufferMerge
通过设置enablePopBufferMerge =true(默认false),在拉消息和ack消息时,可以不发送ck和ack消息,仅仅将数据更新到buffer和commitOffsets中。
PopBufferMergeService#addCk:
broker接收pop请求拉消息,仅仅将checkppint放入buffer和commitOffsets。
PopBufferMergeService#addAk:
broker接收ack请求,匹配buffer中的checkpoint,设置bitmap中ack进度(拉一次消息最多同一个队列拉32条,32条消息对应一个checkpoint,一个ack可更新其中1个bit代表消费成功)。
PopBufferMergeService#scan:
PopBufferMergeService线程扫描时,根据isCkDone方法判断ck是否收到ack,提交消费进度。
PopBufferMergeService#scanCommitOffset:
但开启enablePopBufferMerge的问题是,如果部分消息消费慢(或网络问题收不到ack),会导致消费进度无法提交。
PopBufferMergeService#scan:
对于这种情况,在broker侧就只能通过一些超时时间来控制。
当超时,broker存储checkpoint和ack,直接跳过这条消息,不深入分析。
重试
在pull模式中,重试由consumer发起:
1)将offset直接提交;
2)发送CONSUMER_SEND_MSG_BACK请求,将消息重新投递;
而在pop模式中,offset在pull之后就可能就由broker在PopBufferMergeService后台线程中提交了。
consumer通过changePopVisibleTime的方式进行重试。
consumer changePopVisibleTime
ConsumeMessagePopConcurrentlyService#processConsumeResult:
在consumer侧,如果消费失败,根据情况执行changePopVisibleTime。
如果重试未超过16次,直接执行changePopVisibleTime;
如果重试大于等于16次,根据消息发送时间决策是否执行changePopVisibleTime;
ConsumeMessagePopConcurrentlyService#checkNeedAckOrDelay:
如果消息重试超过16次,且距离消息发送时间超出4h,直接ack消息;
如果消息重试超过16次,但是距离消息发送时间不超出4h,根据距离消息发送时间,找延迟级别,执行changePopInvisibleTime。
DefaultMQPushConsumerImpl#changePopInvisibleTimeAsync:
根据delayLevel延迟级别转换得到一个invisibleTime,发送CHANGE_MESSAGE_INVISIBLETIME请求。
broker changePopVisibleTime
ChangeInvisibleTimeProcessor#processRequest:
对于CHANGE_MESSAGE_INVISIBLETIME请求,broker做了两件事情:
- 发送一个新的ck消息,存储checkpoint;
- 对于老的ck消息,发送一个对应的ack消息;
补偿
拉消息自动提交offset,提高了消费处理速度,但是无法正确判断consumer是否消费成功。
上述流程中,ck和ack消息的作用是为了补偿无法正确处理消费进度的问题,比如:
1)broker拉消息自动提交,consumer消费成功,但broker未收到ACK_MESSAGE;
此时只有ck消息,不存在ack消息,无法判断消费者是否真的消费成功;
2)broker拉消息自动提交,但consumer消费失败,broker收到CHANGE_MESSAGE_INVISIBLETIME;
此时有ck和ack消息(也可能没有ack,被try-catch了,那么情况同1),还有一条新的ck消息,新ck消息的invisibleTime是根据delayLevel计算得到的;
3)broker拉消息自动提交,但consumer消费失败,broker未收到CHANGE_MESSAGE_INVISIBLETIME;
此时只有ck消息,不存在ack消息,无法判断消费者是否真的消费成功;
在AckMessageProcessor中构建了n(默认8)个PopReviveService线程。
每个PopReviveService 负责消费receive topic中的一个队列,匹配ck消息和ack消息。
如果未匹配,代表客户端消费超时或失败。
注:其实这个和事务消息回查机制就比较像了。
PopReviveService线程:
- 拉取receive消息,匹配ck和ack;
- 对于超出invisibleTime的原始消息,重试,提交receive消费进度;
ConsumeReviveObj每轮批处理的上下文对象:
- map:本次捞取的checkpoint;
- sortList:根据receive topic的消息offset排序的checkpoint;
- oldOffset:本轮receive topic消费初始进度;
- newOffset:本轮receive topic消费最终进度;
- endTime:最后一条receive消息的deliverTimeMs(大概是这个意思,并不严格),即popTime+invisibleTime;
拉取receive消息
PopReviveService#consumeReviveMessage:
以group=CID_RMQ_SYS_REVIVE_GROUP,获取receive topic对应队列下的消息。
PopReviveService#consumeReviveMessage:
将拉取到的receive消息,放入ConsumeReviveObj.map,同时ack消息匹配ck消息(bitmap)。
消费receive消息
PopReviveService#mergeAndRevive:
循环处理每个checkpoint,对于超出invisibleTime(broker收到pop请求超过60s)的checkpoint需要处理,最终提交receive topic的消费进度。
PopReviveService#reviveMsgFromCk:
如果ck未匹配ack,即实际消费情况未知,需要查询原始消息并重新发送 。消费重试changeInvisibleTime正是通过这里实现。
PopReviveService#reviveRetry:
发送重试消息,topic=拉消息提到过的 %RETRY%{group}_{topic} ,队列id固定0(一个topic只有一个队列)。
注:最后也会唤醒长轮询客户端。
总结
Rebalance
使用pop消费的前提是,开启broker rebalance,客户端需要满足下面4个条件:
- 使用push api:DefaultMQPushConsumer;
- 关闭clientRebalance:DefaultMQPushConsumer#setClientRebalance(false);
- 并发消费:MessageListenerConcurrently;
- 集群消费:MessageModel.CLUSTERING;
broker rebalance
broker根据队列数量 、consumer数量 和popShareQueueNum的关系执行rebalance。
case1:popShareQueueNum=-1(默认) 或popShareQueueNum >= consumer实例数量-1
broker为每个consumer全量分配queue,实现方式是返回master broker数量个queueId=-1的MessageQueue;
case2:popShareQueueNum < consumer实例数量-1 且 consumer数量 <= queue数量
broker为每个consumer执行多次分配策略。
比如默认使用平均分配策略,8个queue,3个client,popShareQueueNum=1。
index=0的client分配q0-q5,index=1的client分配q3-q7,index=2的client分配q6-q7和q0-q2。
case3:popShareQueueNum < consumer实例数量-1 且 consumer数量 > queue数量
每个consumer分配一个queue,仍然是队列独占。
consumer processQueue
consumer收到broker rebalance分配的MessageQueue。
每个MessageQueue对应一个PopProcessQueue,不再像ProcessQueue一样需要缓存Message管理offset。
Pull
consumer针对每个PopProcessQueue,提交一个PopRequest从broker拉取消息,但是PopRequest不包含offset,默认还是最大拉32条消息。
broker在不满32条消息的情况下,从多个队列拉消息。
- 20%概率,先拉pop重试topic=%RETRY%{consumerGroup}_{topic};
- 如果queueId=-1(默认),即分配所有队列,从随机下标开始的queueId开始拉取消息,直到满32;
- 如果queueId非-1,从指定queueId拉取消息;
- 在没有拉过pop重试topic消息的情况下(80%),如果没满32,再拉pop重试topic;
对于每个拉消息的方法:
- 先要获取queue级别互斥锁 ,保证同一个队列【按顺序】分配n条消息给不同consumer;
- 读取内存消费进度,包含底层offsetTable和pop缓存commitOffsets两部分逻辑;
- 读消息;
- 记录checkpoint;
- 释放queue级别互斥锁;
PopCheckPoint 可以代表一次pop请求针对某个queue成功拉到消息的情况。
记录checkpoint包含三个事情:
- 一个ck消息,topic=rmq_sys_REVIVE_LOG_{clusterName} ,tag=ck,body=PopCheckPoint,用于自动提交补偿;
- 内存缓存lockKey(topic+consumerGroup+queueId)对应checkpoint列表,即PopBufferMergeService#commitOffsets,用于offset提交;
- 内存缓存checkpoint唯一键(topic+consumerGroup+queueId+起始消费进度+pop请求时间戳+broker名)对应checkpoint,即PopBufferMergeService#buffer,用于保证ck消息持久化;
对于没有获取到消息的pop请求(可能因为获取queue级别互斥锁失败),会被挂起,等待新消息到来或长轮询超时(这部分忽略了)。
Consume
消费成功
consumer发送一个ACK_MESSAGE请求给broker。
broker存储一个ack消息,topic=rmq_sys_REVIVE_LOG_{clusterName} ,tag=ack,body=AckMessage。
消费失败
consumer在不超出次数和时间限制的情况下,发送一个CHANGE_MESSAGE_INVISIBLETIME请求给broker。
其中根据失败次数,设置invisibleTime从10s到2h。
broker:
- 存储一个新的ck消息,invisibleTime根据consumer请求指定
- 针对老ck消息存储一个ack消息
消费进度管理
broker开启1个PopBufferMergeService线程,管理所有队列的pop消费进度。
PopBufferMergeService扫描内存PopBufferMergeService#buffer,针对未持久化的checkpoint补偿执行持久化(pull的时候允许ck消息存储失败),持久化成功的checkpoint从buffer中移除。
PopBufferMergeService扫描内存PopBufferMergeService#commitOffsets ,针对持久化成功的checkpoint,在获取queue级别互斥锁 的情况下(与Pull消息互斥 ), 【按顺序】提交offset (内存offsetTable,异步刷盘)。可认为是一种自动提交,即pull消息成功直接提交。
针对pop消费进度查询,会取max(offsetTable,commitOffsets)。
补偿
多种情况下无法确认consumer实际消费结果(类似于事务消息的处理思路) ,所以引入ck和ack消息。
ck消息,代表consumer收到消息,ack消息,代表consumer消费成功。
针对topic=rmq_sys_REVIVE_LOG 的每个队列(默认8个),开启一个PopReviveService线程消费。
只有当ck在invisibleTime(处理pop请求后60s内)时间内收到对应ack消息,才认为consumer实际消费成功 ,否则将重新投递原始消息,投递topic= %RETRY%{group}_{topic} ,队列id固定0。