消费者启动
RebalanceService
负载均衡服务。
一个消息队列仅能被一个消费者消费,但一个消费者可以同时消费多个消息队列。这就涉及到如何将多个消息队列分配给等多个消费者的问题。
RebalanceService
来专门处理多个消息队列和消费者的对应关系,并且提供了多个不同的消费者负载均衡策略,即如何分配消息队列给这些消费者。
有三种情况会触发Consumer进行负载均衡或者说重平衡:
- RebalanceService服务是一个线程任务,由MQClientInstance启动,其每隔20s自动进行一次自动负载均衡。
- Broker触发的重平衡
- Broker收到心跳请求之后如果发现消息中有新的consumer连接或者consumer订阅了新的topic或者移除了topic的订阅, 则Broker发送Code为
NOTIFY_CONSUMER_IDS_CHANGED
的请求给该group下面的所有Consumer,要求进行一次负载均衡。 - 如果某个客户端连接出现连接异常事件EXCEPTION、连接断开事件CLOSE、或者连接闲置事件IDLE,则Broker同样会发送重平衡请求给消费者组下面的所有消费者。
- Broker收到心跳请求之后如果发现消息中有新的consumer连接或者consumer订阅了新的topic或者移除了topic的订阅, 则Broker发送Code为
- 新的Consumer服务启动的时候,主动调用rebalanceImmediately唤醒负载均衡服务rebalanceService,进行重平衡。
通过负载均衡算法为当前消费者分配了新的消息队列之后,需要更新新分配的消息队列MessageQueue和处理队列ProcessQueue的关系。在更新时,如果是顺序消费,调用lock方法请求broker锁定该队列。如果锁定失败,表示新增消息队列失败,这个队列可能还再被其他消费者消费,那么本次重平衡就不再消费该队列。
对于新分配到的消费队列,会新建PullRequest
,这些PullRequest
将会被存入PullMessageService
服务内部的pullRequestQueue
阻塞队列中,后续异步的消费,自动执行拉取消息的请求,这就是Push模式下最初的拉消息请求的来源。
消费者拉取消息
为了提高效率,会为每个 Topic 在~/store/consumequeue
中创建一个目录,目录名为 Topic 名称。在该 Topic 目录下,会再为每个该Topic 的 Queue 建立一个目录,目录名为 queueId。每个目录中存放着若干 consumequeue 文件,consumequeue 文件是 commitlog 的索引文件,可以根据 consumequeue 定位到具体的消息。
commitlog 真正存储消息的⽂件。
消费者消费消息
消费线程池,最小、最大线程数默认20,阻塞队列为无界阻塞队列LinkedBlockingQueue
。
消费者获取到消息后包装成ConsumeRequest
提交到并发消费线程池。ConsumeRequest
实现Runnable
接口,并发消费和顺序消费是两个不同的ConsumeRequest
并发消费
顺序消费
顺序消费服务ConsumeMessageOrderlyService
在启动时,会启动一个定时任务,该定时任务请求Broker进行对MessageQueue
的锁定,只有锁定成功的MessageQueue
,对应的ProcessQueue
才会设置为锁定状态
顺序消费在ConsumeRequest
执行时,才会从ProcessQueue
拿消息。
小结
多线程和顺序性。顺序消费和并发消费实际上都是使用线程池消费,但是不同的是,对于同一个消息队列的消息,并发消费可能有多条线程并发的消费消息,提升了消息速度,但是没有顺序性。
顺序消费则通过一系列锁,保证同一时刻对于同一个队列只有一个线程去消费它,注意是“只有一个线程”而不是“同一个线程”,因此有可能你先发送的消息被ThreadA消费了,但是你发送的的第二个消息被ThreadB消费了,这是完全正确的,因为他们并不是同时消费的。但是对于不同的队列,顺序消费则不能保证消费有序性,因为不同的队列有不同的锁。
消息重试
并发消费和顺序消费对于消费失败的消息均会有消息重试机制
顺序消费的保证
- broker端的分布式锁
- messageQueue的本地synchronized锁
- ProcessQueue的本地consumeLock