PullRequest简介
PullRequest: 一个Topic下一个消费组的消费快照。
String consumerGroup // 消费组
long nextOffset // 下次拉取位置
MessageQueue messageQueue // 消息队列,原数据
String topic // Topic名称
String brokerName // Broker名称
int queueId // 消息队列Id
ProcessQueue processQueue // 队列消费快照
ReadWriteLock treeMapLock // treeMap的读写锁
TreeMap<Long, MessageExt> msgTreeMap // 保存从MessageQueue中获取到的未处理的消息
AtomicLong msgCount // 还有多少消息未被处理
AtomicLong msgSize // 未处理消息的大小
boolean locked // 顺序消息第一把锁:messageQueue的分布式锁
ReentrantLock consumeLock // 消息消息第三把锁:防止ProcessQueue被移除
消息消费前置 - 创建PullRequest
RebalanceService#run ->waitForRunning(利用CountDownLatch2等待20s,每次#reset)
-> MQClientInstance#doRebalance
-> 循环MQClientInstance.consumerTable(ConcurrentMap<String/* group */, MQConsumerInner>)一个组对应一消费个实例
-> MQConsumerInner(DefaultMQPushConsumerImpl)#doRebalance
-> RebalanceImpl#doRebalance
-> 循环ConcurrentMap<String /* topic */, SubscriptionData> subscriptionInner 一个消费者可以订阅多个Topic,如自定义的Topic、Group的消息重试Topic
-> #rebalanceByTopic(根据topic负载)
-> 获取topic下的消息队列mqSet,获取topic下的实例cidAll
-> 选择分配策略(默认平均分配) -> 将消息队列分配到当前实例(可能是多个)allocateResultSet
-> #updateProcessQueueTableInRebalance(topic,allocateResultSet)
-> 循环RebalanceImpl.processQueueTable,删除不在allocateResultSet中的队列(有锁则无法删除)
-> 循环allocateResultSet。如果分配的队列已经在processQueueTable中存在了跳过,如果是顺序消息且未获取到锁则跳过
-> 创建ProcessQueue放到processQueueTable -> 创建PullRequest
-> RebalancePushImpl#dispatchPullRequest
->DefaultMQPushConsumerImpl#executePullRequestImmediately
-> PullMessageService#executePullRequestImmediately
-> PullMessageService.pullRequestQueue#put(pullRequest)
消息消费流程
PullMessageService#run -> pullRequestQueue#take 阻塞获取拉取请求
-> MQConsumerInner(DefaultMQPushConsumerImpl)#pullMessage
-> 通过PullRequest.processQueue判断是否需要限流,需要的话会阻塞50ms
-> 超过1000条消费未消费,ProcessQueue#getMsgCount
-> 超过100M的消息未消费,ProcessQueue#getMsgSize
-> 最小与最大offset相差大于2000,ProcessQueue#maxSpan(msgTreeMap#lastKey - msgTreeMap#firstKey)
-> 顺序消息需要获取到ProcessQueue锁才能执行,否则等3s再次获取
-> 创建拉取回调,PullCallBack#onSuccess、#onException
-> PullAPIWrapper#pullKernelImpl
-> 获取broker地址:MQClientInstance#findBrokerAddressInSubscribe
-> 封装拉取请求参数RequestHeader:哪个Topic、哪个Group、哪些Tag、
从哪开始拉取(PullRequest.nextOffset)、拉取消息数量(32)、没消息时的挂起时间(15s)
-> 封装要请求Broker的参数:RemotingCommand#createRequestCommand(RequestCode.PULL_MESSAGE, requestHeader)
-> MQClientAPIImpl#pullMessage,选择同步、异步、oneWay发送方式(同步有返回值,异步没有)
异步流程:
-> 创建InvokeCallback#operationComplete
-> 调用Remoting包下的类发送拉取消息请求:RemotingClient#invokeAsync(broker地址,requestHeader)
-> 创建通道,NettyRemotingClient#getAndCreateChannel(broker地址)
-> 生成opaque -> 新建ResponseFuture -> opaque为key,ResponseFuture为value放到responseTable中
-> channel#writeAndFlush,channel添加监听器ChannelFutureListener#operationComplete
-> channel发送成功回调,失败后根据opaque从responseTable中移除ResponseFuture
broker接收到请求后的处理:
RemotingService(请求父类) -> NettyRemotingClient、NettyRemotingService的父类
NettyRemotingAbstract(抽象模版类) -> NettyRemotingClient、NettyRemotingService的父类
NettyRemotingClient#start ->
ChannelOption#handler -> new ChannelInitializer#initChannel -> 设置ChannelPipeline, 添加NettyClientHandler
NettyClientHandler#channelRead0 ->
NettyRemotingAbstract#processMessageReceived ->
NettyRemotingAbstract#processResponseCommand ->
-> responseTable.remove(opaque) // 根据opaque从responseTable中移除ResponseFuture
异步有Callback:executeInvokeCallback
同步执行ResponseFuture#putResponse
-> ResponseFuture.countDownLatch#countDown
-> SemaphoreReleaseOnlyOnce#release
PullCallBack拉取回调、InvokeCallback调用请求回调 流程
#invokeCallback -> #pullCallback
无消息返回-PullSatatus: NO_NEW_MSG、NO_MATCHED_MSG:
-> 如果没有新消费或未匹配到消息,重新将PullRequest放回PullMessageService.pullRequestQueue
有消息返回的-PullSatatus: FOUND:
-> PullRequest.processQueue#putMessage // 将拉取回来的消息放到 ProcessQueue的msgTreeMap中,使用treeMapLock#writeLock写锁进行加速
-> ConsumeMessageService#submitConsumeRequest // 创建消费线程,开始消费消息。子类有ConsumeMessageConcurrentlyService、ConsumeMessageOrderlyService
-> 创建ConsumeRequest imp Runnable
-> 如果消息数量 <= counsumer一次消费的数量,直接执行#submit,否则封装counsumer一次消费的数量后,再执行#submit
-> 当#submit发生RejectedExecutionException时,执行#submitConsumeRequestLater,5S之后再次#sbumit
-> ConsumeRequest#run -> MessageListener#consumeMessage // 并发消费 MessageListenerConcurrently、顺序消费 MessageListenerOrderly
-> 根据消费者返回的status,进行处理 // 并发消费:返回null、跟异常都认为是稍后重新消费、顺序消费则认为是挂起消费者一会再消费(1s)
-> ConsumeMessageConcurrentlyService#processConsumeResult
-> #sendMessageBack 失败消息重新发送,Topic变为 %RETRY% 。如果需要重试idx为-1,循环msgSize发送
-> 删除消息,返回第一个offset,ProcessQueue#removeMessage,这里会使用treeMapLock.writeLock()进行加锁
-> 根据返回的offset,更新offset,RemoteBrokerOffsetStore.ConcurrentMap<MessageQueue, AtomicLong> offsetTable
顺序消费
第一把锁ProcessQueue.locked,boolean类型
设置locked的两种方式:
1. 定时设置锁:消费者启动时,定时器每20s执行加锁,#lockAll,传入一批messageQueue,返回成功锁住的,如果加锁成功设置locked为true,不成功为false。
2. 创建PullRequest:rebalance过程中,如果发现消费者负载到了新实例,并且是顺序消息则调用#lock,单独给新队列加锁
顺序消息三把锁
第一把锁:
消费消息时先判断是否能消费:DefaultMQPushConsumerImpl#pullMessage,当顺序消息时只有locked为true,才进行拉取
第二把锁:
messageQueueLock,synchronzied修饰
第三把锁:
ProcessQueue.consumeLock,ReentrantLock,对处理队列加锁,防止被负载均衡时移除
订阅关系不一致
结论:
1. 同Topic下的不同client,都会向broker发送心跳,broker会以最后一个client实例的订阅关系为准
2. broker在处理client的消息拉取请求时,会使用最新的订阅关系去过滤tag
例子:
clientA的tag: tag1、tag2,clietB的tag:tag3,clientB为最后一个注册者,此时Broker的订阅关系为tag3。
当clientA的拉取请求到broker时,虽然消息为tag1、tag3 但是订阅关系为tag3,所以拉取不到消息
Client端:
#subscribe
1. 本地缓存topic的订阅信息,subscriptionInner
2. 通过心跳包向broker发送当前消费者的订阅消息,subcriptionTable
client的订阅心跳:
1. 定时同步broker的订阅信息到client。使用时间戳来
2. 定时将client的订阅关系发送到broker
拉取消息:
1. PullCallback#onSuccess
2. PullApiWrapper#processPullResult,会再次进行消息过滤
Broker端:
处理consumer的心跳请求时,设置topic的订阅信息,包括tag信息。
1. 处理心跳。ClientManageProcessor#heartBeat,处理RequestCode.HEART_BEAT的请求
2. 更新topic的订阅信息,subscriptionTable。ConsumerGroupInfo#updateSubscription
a. ConcurrentMap<String/* Topic */, SubscriptionData> subscriptionTable
b. 只有最新的版本号才放入subscriptionTable
处理拉取消息:
1. PullMessageProcessor#processRequest
2. DefaultMessageStore#getMessage
3. MessageFilter#isMatchedByConsumeQueue,根据tag过滤消息
其它
- SemaphoreReleaseOnlyOnce#release 、使用场景
内置了Semaphore
ResponseFuture#release
1. NettyRemotingAbstract#processResponseCommand,处理Broker响应结果;
2. NettyRemotingAbstract#invokeAsyncImpl,channel.writeAndFlush异常时;
3. NettyRemotingAbstract#invokeAsyncImpl,ChannelFutureListener#operationComplete,ChannelFuture#isSuccess返回false
- ProcessQueue.treeMapLock:ReentrantReadWriteLock使用场景
1. ProcessQueue#putMessage
2. ProcessQueue#removeMessage
- 同步消息哪种场景下使用
应用与DefaultMQPullConsumerImpl,我们使用的是DefaultMQPushConsumerImpl
- ACK有几种确认机制
- 并发消费(ConsumeConcurrentlyStatus):
- 消费成功(CONSUME_SUCCESS)
- 稍等重新消费(RECONSUME_LATER): MessageListener#consumeMessage后,返回null或者异常都认为是RECONSUME_LATER
- 顺序消费(ConsumeOrderlyStatus): (消费成功)SUCCESS、(暂停一会儿再消费)SUSPEND_CURRENT_QUEUE_A_MOMENT、(已弃用)COMMIT、ROLLBACK
- 并发消费(ConsumeConcurrentlyStatus):
- 顺序消息为什么要设置自动提交
好像是之前的事物消费。
之前顺序消息有COMMIT、ROLLBACK等状态,如果为自动提交则 COMMIT、ROLLBACK、SUCCESS时都会执行ProcessQueue#commit;
如果不设置自动提交则只有COMMIT时会执行ProcessQueue#commit;