概述

本文目的在于将消息消费的流程梳理完毕,使自己包括读者能够对 RocketMQ 的消息消费流程有清晰的认识。

主要包含以下内容:

  • 相关概念介绍
  • 消费端的队列分配,即负载均衡机制
  • 消息拉取的实现机制
  • 并发消费,顺序消费的实现机制

消费模式

Java rocketmq 针对不同tag消费 rocketmq并发消费原理_java

消费者类型

Java rocketmq 针对不同tag消费 rocketmq并发消费原理_kafka_02


注意:在最新发布的 RocketMQ 中,已将 DefaultMQPullConsumer 类标记为弃用,预计在 2022 会将这个类移除,对应的替代类为 DefaultLitePullConsumer。

Java rocketmq 针对不同tag消费 rocketmq并发消费原理_加锁_03

消费方式

分为并发消费和顺序消费。

  • 并发消费,一个队列中的消息可同时被消费者的多个线程并发消费
  • 顺序消费,一个队列中的消息同一时间只能被一个消费者的一个线程消费,通过这种方式达到顺序消费的效果

消费流程

主要分析消息推送场景下的消费流程,下图展示了过程中几个重要的步骤

  • 队列分配,即消费端的负载均衡,由 RebalanceImpl 组件实现
  • 拉取消息,在队列分配完成的基础上,从 Broker 中拉取消息 ,由 PullMessageService 组件实现;拉取完成后对应的 PullCallback 处理
  • 消息处理,触发消费逻辑以及后续的消费结果处理,由 ConsumeMessageService 组件实现

Java rocketmq 针对不同tag消费 rocketmq并发消费原理_消息队列_04

队列分配

队列分配目的在于指定消费者负责的队列集合,分配前需要明确几点:

  • 该 Topic 存在多少队列
  • 该 Topic 存在多少消费者
  • 队列如何分配给消费者,即负载均衡算法(默认是平均分配的算法)

由 RebalanceService 组件定时触发,周期为 20 s 一次。

队列分配流程如下:

  • 获取指定 Topic 下的消息队列集合
  • 如果是广播模式,则不需要进行负载均衡,消费者直接负责所有消息队列
  • 集群模式则需要获取指定 Topic 的所有消费者集合,根据负载均衡算法将消息队列分配给消费者
  • 消息队列分配完毕后,则需要为每个消息队列创建对应的任务队列,即 ProcessQueue
  • 为每个任务队列创建对应的消息拉取任务,后续消息拉取服务会定时扫描任务池进行消息拉取操作

Java rocketmq 针对不同tag消费 rocketmq并发消费原理_加锁_05

拉取消息

Push 型的消息消费方式,用户不需要关心消息的获取动作,感觉就像服务端主动把消息推送给了消费者。

这归功于消息拉取服务 PullMessageService,它是一个后台运行地服务,从任务池中获取并执行消息拉取任务,可以看到只要任务队列中存在任务,就会执行任务,反之则阻塞。

Java rocketmq 针对不同tag消费 rocketmq并发消费原理_消息队列_06

消息拉取回调

消息拉取完毕的后续处理逻辑:

  • 如果成功拉取到消息,则将消息加入到待处理任务队列 ProcessQueue,并提交一个消费请求给 ConsumeMessageService,提交下一次消息拉取任务
  • 如果没有成功拉取到消息,则根据服务端返回的 Offset 进行校正处理,重新提交消息拉取任务

Java rocketmq 针对不同tag消费 rocketmq并发消费原理_kafka_07

消息消费

消息拉取完毕之后,会提交一个消费任务给 ConsumeMessageService 进行处理。

ConsumeMessageService 有两个实现类:

  • 并发处理,对应实现类为 ConsumeMessageConcurrentlyService
  • 顺序处理,对应实现类为 ConsumeMessageOrderlyService
并发消费

看到消费任务 ConsumeRequest 的定义, 它是 ConsumeMessageConcurrentlyService 的内部类,实现了 Runnable 接口,因此可以被线程池直接执行,任务主要是触发消费逻辑,以及对消费结果的处理。

Java rocketmq 针对不同tag消费 rocketmq并发消费原理_加锁_08

如果消费成功,则把此次消费的消息从任务队列中移除,并更新消费位点。

如果消费失败且为集群模式,则把消息重发至 Broker,等待后续重新消费(重试逻辑)。

注意:如果消息重发至 Broker 失败了,则会将消息重新提交至任务队列中等待消费者处理。

流程如下:

Java rocketmq 针对不同tag消费 rocketmq并发消费原理_rabbitmq_09

顺序消费

RocketMQ 实现顺序消费的思路比较简单,即要求生产者把消息发送到同一个队列中(分局部有序和全局有序,这里不详细说明),利用队列天然的有序性实现顺序消费。

但只是把消息发送到同一个队列,并不能保证顺序消费。

例如下面两种场景就无法保证顺序消费:

  • 消费者A正在消费队列A的消息,此时消费者B发生了队列的负载均衡,也分配到了队列A,在同一时间相当于有两个消费者可以同时消费一个队列的消息
  • 当前队列A由一个消费者A负责,但消费者A内部可以进行并发消费,即多个消费线程同时消费队列A的消息

因此还需要结合锁的机制来实现顺序消费:

  • 同一时间一个队列只能分配给一个消费者,通过给 Broker 端队列上锁实现
  • 同一时间一个队列只能有一个消费线程进行消费,通过给本地队列上锁实现

在队列负载均衡阶段,如果是顺序消费,会向 Broker 发起队列加锁请求,如果加锁成功则创建对应的任务队列及消息拉取请求,反之不创建。

Java rocketmq 针对不同tag消费 rocketmq并发消费原理_消息队列_10

ConsumeMessageOrderlyService 在启动后会定时向 Broker 发送队列加锁的请求,目的是续期锁。

具体的加锁操作如下:

  • 获取消费者负责的消息队列集合 HashMap<String, Set>,Map 的 Key 是 Broker 名称,Value 则是所属该 Broker 的消息队列集合
  • 依次对每个 Broker 下的消息队列进行加锁操作,Broker 会响应加锁成功的消息队列集合
  • 如果消息队列加锁成功,则将本地对应的任务队列设置为加锁成功的状态;反之则设置成加锁失败状态

Java rocketmq 针对不同tag消费 rocketmq并发消费原理_java_11

消费的过程中则通过对本地队列加锁来实现同一时间一个队列只能有一个消费线程进行消费。

看到消费任务 ConsumeRequest 的定义, 它是 ConsumeMessageOrderlyService 的内部类,不同于之前并发消费的任务,可以看到主要区别在于消费时增加了本地队列的加锁操作,以及锁状态的校验。

Java rocketmq 针对不同tag消费 rocketmq并发消费原理_消息队列_12

值得注意的是,顺序消费时如果消费失败,会直接将消息放回任务队列中等待重新消费,且重试次数默认是 Integer.MAX_VALUE。