DefaultMQPushConsumerImpl类是核心类
一个Consumer Group中的各个Consumer实例分摊去消费消息,即一条消息只会投递到一个Consumer Group下面的一个实例。
实际上,每个Consumer是平均分摊Message Queue的去做拉取消费。例如某个Topic有3个Queue,其中一个Consumer Group 有 3 个实例(可能是 3 个进程,或者 3 台机器),那么每个实例只消费其中的1个Queue消息。
而由Producer发送消息的时候是轮询所有的Queue,所以消息会平均散落在不同的Queue上,可以认为Queue上的消息是平均的。那么实例也就平均地消费消息了。
这种模式下,消费进度 (Consumer Offset) 的存储会持久化到Broker 。
消息将对一个Consumer Group下的各个Consumer实例都投递一遍。即使这些 Consumer 属于同一个Consumer Group,消息也会被Consumer Group 中的每个Consumer都消费一次。
这种模式下,消费进度 (Consumer Offset) 会存储持久化到实例本地 。
在集群消费模式下,每条消息只需要投递到订阅这个topic的Consumer Group下的一个实例即可。RocketMQ采用主动拉取的方式拉取并消费消息,在拉取的时候需要明确指定拉取哪一条message queue。
而每当实例的数量有变更,都会触发一次所有实例的负载均衡,这时候会按照queue的数量和实例的数量平均分配queue给每个实例。
默认的分配算法是AllocateMessageQueueAveragely,还有另外一种平均的算法是AllocateMessageQueueAveragelyByCircle,也是平均分摊每一条queue,只是以环状轮流分queue的形式
如下图:
需要注意的是,集群模式下,queue都是只允许分配只一个实例,这是由于如果多个实例同时消费一个queue的消息,由于拉取哪些消息是consumer主动控制的,那样会导致同一个消息在不同的实例下被消费多次,所以算法上都是一个queue只分给一个consumer实例,一个consumer实例可以允许同时分到不同的queue。
通过增加consumer实例去分摊queue的消费,可以起到水平扩展的消费能力的作用。而有实例下线的时候,会重新触发负载均衡,这时候原来分配到的queue将分配到其他实例上继续消费。
但是如果consumer实例的数量比message queue的总数量还多的话,多出来的consumer实例将无法分到queue,也就无法消费到消息,也就无法起到分摊负载的作用了。所以需要控制让queue的总数量大于等于consumer的数量。
由于广播模式下要求一条消息需要投递到一个消费组下面所有的消费者实例,所以也就没有消息被分摊消费的说法。
在实现上,其中一个不同就是在consumer分配queue的时候,所有consumer都分到所有的queue。
一般我们在消费时使用回调函数的方式,使用得最多的是并发消费,消费者客户端代码如下:
在RocketMQ的消费时,整体流程如下:
在消费者启动之后,第一步都要从NameServer中获取Topic相关信息。这一步设计到组件之间的交互,RocketMQ使用功能号来设计的。
最终在MQClientAPIImpl类中完成调用
在消费消息前,需要获取当前分组已经消费的相关信息:ConsumerList
如果是广播消费模式,则不需要从服务器获取消费进度(广播消费模式把进度在本地<消费端>进行存储)
而广播消费模式,则需要从服务器获取消费进度相关信息,具体如下:
在分配完消费者对应的Queue之后,如果是集群模式的话,需要获取这个消费者对应Queue的消费Offset,便于后续拉取未消费完的消息。
进入RebalancePushImpl类
最终进入DefaultMQPushConsumerImpl类的pullMessage方法
因为RocketMQ的推模式是基于拉模式实现的,因为拉消息是一批批拉,所以不能做到拉一批提交一次偏移量,所以这里使用定时任务进行偏移量的更新。
顺序消费代码:顺序消费的流程和并发消费流程整体差不多,唯一的多的就是使用锁机制来确保一个队列同时只能被一个消费者消费,从而确保消费的顺序性
ConsumeMessageOrderlyService类
这里有一个定时任务,是每个20秒运行一次(周期性的去续锁,锁的有效期是60S)
消费的流程中,尤其是针对顺序消息,会有卡死的现象,由于顺序消息中需要到Broker中加锁,如果消费者某一个挂了,那么在Broker层是维护了60s的时间才能释放锁,所以在这段时间只能,消费者是消费不了的,在等待锁。
另外如果还有Broker层面也挂了,如果是主从结构,获取锁都是走的Master节点,如果Master节点挂了,走Slave消费,但是slave节点上没有锁,所以顺序消息如果发生了这样的情况,也是会有卡死的现象。
在并发消费的时候,当我们启动了非常多的消费者,维护了非常多的topic的时候、或者queue比较多的时候,可以看到消费的流程的交互是要启动多线程,也要做相当多的事情,所以会感觉要启动较长的时间才能消费。