消息重试策略
RocketMQ会为每个消费者组都设置一个Topic名称为“%RETRY%+consumerGroup”的重试队列(这里需要注意的是,这个Topic的重试队列是针对消费组,而不是针对每个Topic设置的),用于暂时保存因为各种异常而导致Consumer端无法消费的消息。
考虑到异常恢复需要一些时间,RocketMQ会为重试队列设置多个重试级别,每个重试级别都有与之对应的重新投递延时,重试次数越多投递延时就越大。RocketMQ对于重试消息的处理是先保存至Topic名称为“SCHEDULE_TOPIC_XXXX”的延迟队列中,后台定时任务按照对应的时间进行Delay后重新保存至“%RETRY%+consumerGroup”的重试队列中。
DefaultMQPushConsumerImpl
public synchronized void start() throws MQClientException {
//初始化
this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook);
...
if (this.getMessageListenerInner() instanceof MessageListenerOrderly) {
this.consumeOrderly = true;
this.consumeMessageService =
new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner());
} else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) {
this.consumeOrderly = false;
this.consumeMessageService =
new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner());
}
this.consumeMessageService.start();
}
- 初始化MQClientInstance。
- 初始化consumeMessageService【有序消费、并发消费】。
MQClientInstance
该实例的启动是通过自定义的生产者、消费者调用#start方法触发。
public MQClientInstance(ClientConfig clientConfig, int instanceIndex, String clientId, RPCHook rpcHook) {
...
this.clientId = clientId;
this.mQAdminImpl = new MQAdminImpl(this);
this.pullMessageService = new PullMessageService(this);
this.rebalanceService = new RebalanceService(this);
...
}
- 初始化PullMessageService。
- 初始化RebalanceService。
public void start() throws MQClientException {
synchronized (this) {
switch (this.serviceState) {
case CREATE_JUST:
this.serviceState = ServiceState.START_FAILED;
// If not specified,looking address from name server
if (null == this.clientConfig.getNamesrvAddr()) {
this.mQClientAPIImpl.fetchNameServerAddr();
}
// Start request-response channel
this.mQClientAPIImpl.start();
// Start various schedule tasks
this.startScheduledTask();
// Start pull service
this.pullMessageService.start();//启动拉取消息任务
// Start rebalance service
this.rebalanceService.start();// 启动消费端负载均衡任务
// Start push service
this.defaultMQProducer.getDefaultMQProducerImpl().start(false);
this.serviceState = ServiceState.RUNNING;
break;
case START_FAILED:
throw new MQClientException("The Factory object[" + this.getClientId() + "] has been created before, and failed.", null);
default:
break;
}
}
}
PullMessageService
PullMessageService 从服务端拉取到消息后,会根据消息对应的消费组,转给该组对应的 ProcessQueue,而 ProcessQueue 是 MessageQueue 在消费端的重现、快照。 PullMessageService 从消息服务器默认每次拉取 32 条消息,按消息的队列偏移量顺序存放在 ProcessQueue 中,PullMessageService 然后将消息提交到消费者消费线程池,消息成功消费后从 ProcessQueue 中移除。
PullMessageService是消息拉取服务线程【ServiceThread】。MQClientInstance在启动时(调用start方法)会调用PullMessageService#start方法来启动消费者消息拉取线程。
public void run() {
while (!this.isStopped()) {//不断轮询拉取broker端的消息
PullRequest pullRequest = this.pullRequestQueue.take();//主要是轮询获取当前消费端实例消费的所有队列信息
this.pullMessage(pullRequest);
}
}
队列PullRequestQueue中的元素是线程RebalanceService执行的任务赋值完成的。
private void pullMessage(final PullRequest pullRequest) {
final MQConsumerInner consumer =
this.mQClientFactory.selectConsumer(pullRequest.getConsumerGroup());
DefaultMQPushConsumerImpl impl = (DefaultMQPushConsumerImpl) consumer;
impl.pullMessage(pullRequest);
}
RebalanceService
RebalanceService【ServiceThread】,start方法会启动一个后台线程,确保每隔一段时间(默认20秒)会调用一次MQClientInstance的doRebalance方法。
public void run() {
while (!this.isStopped()) {
this.waitForRunning(waitInterval);
this.mqClientFactory.doRebalance();//MQClientInstance.doRebalance
}
}
开始处理消费端的负载均衡。
public void doRebalance() {
for (Map.Entry<String, MQConsumerInner> entry : this.consumerTable.entrySet()) {
MQConsumerInner impl = entry.getValue();//DefaultMQPushConsumerImpl
impl.doRebalance();
}
}
DefaultMQPushConsumerImpl
public void doRebalance() {
if (!this.pause) {
// RebalancePushImpl
this.rebalanceImpl.doRebalance(this.isConsumeOrderly());
}
}
RebalancePushImpl
public void doRebalance(final boolean isOrder) {
Map<String, SubscriptionData> subTable = this.getSubscriptionInner();
if (subTable != null) {
for (final Map.Entry<String, SubscriptionData> entry : subTable.entrySet()) {
final String topic = entry.getKey();
this.rebalanceByTopic(topic, isOrder);//负载均衡分配算法
}
}
this.truncateMessageQueueNotMyTopic();
}
消费端实例与队列分配算法:
- 平均分配策略(默认)(AllocateMessageQueueAveragely)
- 环形分配策略(AllocateMessageQueueAveragelyByCircle)
- 手动配置分配策略(AllocateMessageQueueByConfig)
- 机房分配策略(AllocateMessageQueueByMachineRoom)
- 一致性哈希分配策略(AllocateMessageQueueConsistentHash)
Mq中平均分配策略的实现方式:
//consumerGroup:表示当前GroupId
//currentCID:当前消费端实例ID,表达方式为 IP@PORT@consumer,
//mqAll:当前topic下所有的队列集合
//cidAll:订阅当前GroupId的所有消费端实例,即currentCID集合
public List<MessageQueue> allocate(String consumerGroup, String currentCID, List<MessageQueue> mqAll,List<String> cidAll) {//所有集合都是经过排序处理的
List<MessageQueue> result = new ArrayList<MessageQueue>();
int index = cidAll.indexOf(currentCID);//获取当前currentCID在集合列表cidAll中下标
int mod = mqAll.size() % cidAll.size();//判断所有消费端实例是否可以平均分配所有队列,mod为0表示平均分配,否则无法平均分配
int averageSize =
mqAll.size() <= cidAll.size()
? 1 //表示消费端实例个数多余队列个数,消费端实例无法平均获取监听队列
: (
mod > 0 && index < mod
? mqAll.size() / cidAll.size() + 1 // topic队列总数为15,消费端实例个数为4,mod 为3
: mqAll.size() / cidAll.size() // 每个消费端平均监听的队列数
);
int startIndex = (mod > 0 && index < mod) ? index * averageSize : index * averageSize + mod;
int range = Math.min(averageSize, mqAll.size() - startIndex);
for (int i = 0; i < range; i++) {
result.add(mqAll.get((startIndex + i) % mqAll.size()));//
}
return result;
}
目的是获取当前消费端实例被分配到的队列。