【保姆级别带你看懂源码】RocketMQ如何拉取消息消费并更新消费位置的?

412 阅读6分钟

怎么拉取消息的

拉取消息主要关注3个逻辑:

  1. 消费者的负载均衡 :确定该消费者处理多少个queue,以及当前queue消费的进度。
  2. 对每个queue组装请求体丢入拉取消息的线程执行。
  3. 拉取消息的后台线程从请求队列取请求发送到broker。

我们抛开源码的其他部分重点看下这3个步骤到底是怎么做的。

负载均衡

在初始化消费者客户端的时候会默认使用一个平均分配的负载均衡策略

image.png

那么在什么时候会调用呢?

在客户端调用start()方法后, 会执行到mQClientFactory.start();方法,在这里面会启动负载均衡的线程。

image.png

来看下负载均衡线程是怎么处理的。

image.png 基本可以理解为默认20秒执行一次负载均衡策略。

MQ的负载均衡是基于topic来处理的,每个topic都有多个queue

来看下具体的源码:

image.png

image.png

在将topic传入后,会先从获取一下这个topic的所有queue信息,随后从Broker拉取该topic下的所有消费者信息下来。(因为所有的消费者启动时都会定时发送心跳给所有的Broker,默认30秒发送一次)。

这样知道了所有的queue和消费者信息后,怎么做负载均衡就由对应的策略类来处理了.

这里我们看下默认的负载均衡怎么处理的,对映源码位置: AllocateMessageQueueAveragely#allocate

image.png

看代码比较难理解,说一个简单的例子,假设该topic有10个queue,共有2个消费者,那么就每个消费者处理5个queue,假设3个消费者,就会出现433的分配情况,尽量保持平均消费。

如果你有4个queue,但是消费者有5个,那么最后一个消费者就不会消费了。

确认了该消费者处理哪些queue之后就需要向Broker拉取消息了。

那么怎么拉的呢?

组装请求体

还是在负载均衡的代码中,在确定完queue之后会调用updateProcessQueueTableInRebalance

image.png

进去看关键代码:

image.png

它会遍历所有的queue,从Broker上拉取当前消费者的消费位置. 当然这也只有集群消费会这样拉取。

看下消费位置在Broker上是如何保存的。

image.png 会有一个文件叫做consumerOffset.json它会记录各个消费者消费的topic的每个queue的进度。

这里面的进度位置指向的是consumerQueue目录下的文件位置。

image.png 每条消息会以20字节长度存入consumerQueue中。

假如当前位置是0,那么0*20 = 0,从consumerQueue中读取0到20个字节的数据,取出其中的消息位置即可。

获取到这些信息后,给每个queue组装请求体随后丢入请求队列。

image.png 看下是怎么处理的:

image.png 其实最后都会走到这里:

image.png 往一个LinkedBlockingQueue<PullRequest>丢入信息,那么必然有一个线程会从里面取出处理,这个线程在哪呢?

还记得在哪里启动负载均衡的吗? 在它的上面就是启动请求线程的。

image.png 那么是怎么处理的呢?

image.png 这里也是个策略模式,集群消费和广播消费会走到不同的逻辑中,这里咱们只说集群消费。

拉取消息

源码DefaultMQPushConsumerImpl#pullMessage

在拉取消息时,会做什么处理呢?

image.png 第一点的处理: 判断堆积消息的总数是否大于了配置值 默认是1000, 这个为了防止拉取消息过多消费不了。

第二点的处理: 判断堆积的消息总大小是否超过了配置值 默认是100M 这个是为了防止消息过多出现OOM。

第三点的处理: 判断消费的位置是否总是在中间,什么意思呢? 这里需要看到提交消费进度那就能明白了,总之就是为了避免消费进度不更新的情况。

最后看到执行请求的地方:

image.png

一次拉取多少消息就是由画横线的地方决定,默认是32条。

拉取消息成功后会调用最后一个参数,那是一个回调函数

接下来就看拉取消息后是怎么处理消息的。

怎么处理消息

看下该回调函数的位置:

image.png 具体源码就先不说了,咱们关注它的重点代码;

image.png

取到消息后,会把所有消息放进processQueue中,随后调用submitConsumeRequest这里也是一个策略,并发消费和顺序消费是不同的逻辑,这里我们先关注并发消费。

image.png 这里面有一个关键参数: consumeBatchSize 默认是1,意思是每次每个runnable只处理一条消息.

可以看到,这里遍历消息列表,给每个消息组装成ConsumeRequest类,丢入到消费线程池中,线程池默认是20个线程,使用LinkedBlockingQueue无界队列。

那么关键就在于这个ConsumeRequest是怎么处理的。

源码位于: ConsumeMessageConcurrentlyService.ConsumeRequest#run()

image.png 它会先重置消息的重试topic名称,随后调用客户端的消费逻辑,得到返回值,也就是消费状态。

这样也就完成了一条消息的消费,那么最后是怎么提交进度的呢?

怎么更新消费进度

看一下关键源码:ConsumeMessageConcurrentlyService.processConsumeResult()

image.png

这里我们关注一下关键的地方。

第一点:集群消费模式下,如果消费失败是会立即发送一个重试消息到重试队列的。

第二点:这里开始处理当前的消费进度。

第三点:这里更新当前消费进度,不过是更新内存中的值,有一个定时任务定时往Broker发送。

看下第二点的源码:

image.png

看到画横线的地方,不管你当前是消费的哪条消息,它始终返回当前内存中第一条消息的位置为最新消费点。

所以,如果消费的一直都是中间位置的消息,那么消费点就一直不会更新,那么在重启后就会重复消费了。

总结

消费者在启动时会向所有Broker注册心跳, 执行负载均衡时会从Broker获取所有的消费者信息,随后执行负载均衡逻辑,分配完所有的queue之后开始组装拉取消息的请求体丢入请求队列,由后台线程一个一个处理,拉取到消息后丢给具体的消费类处理。

并发消费则是走ConsumeMessageConcurrentlyService#submitConsumeRequest()

顺序消费则是走ConsumeMessageOrderlyService#submitConsumeRequest()

并发消费更新位置总是取当前第一条消息的位置,后台定时任务隔一段时间就发送给Broker持久化,我们也知道了消费位置在Broker中是如何存储的以及如何取出消息的。