RocketMq学习-消息消费流程

351 阅读6分钟

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,不成功为false2. 创建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#invokeAsyncImplchannel.writeAndFlush异常时;
3. NettyRemotingAbstract#invokeAsyncImplChannelFutureListener#operationCompleteChannelFuture#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
  • 顺序消息为什么要设置自动提交
好像是之前的事物消费。
之前顺序消息有COMMITROLLBACK等状态,如果为自动提交则 COMMITROLLBACK、SUCCESS时都会执行ProcessQueue#commit;
如果不设置自动提交则只有COMMIT时会执行ProcessQueue#commit