本文基于RocketMQ 4.7.1版本
前面两篇文章介绍了DefaultMQPushConsumer和DefaultMQPullConsumer消费消息的原理,由此我们知道了
本文主要介绍DefaultMQPushConsumer消费失败后如何自动发起重新消费。
从文章《RocketMQ-DefaultMQPushConsumer消费消息原理详解》中可以知道,消息从broker拉取回后,会转发给ConsumeMessageConcurrentlyService处理。
ConsumeMessageConcurrentlyService内部启动一个线程池异步处理消息,线程池最多可以有20个线程。每个消息放入到线程池之前,会使用ConsumeRequest进行封装,代码如下:
//msgs:拉取回的消息集合
//processQueue:是ProcessQueue对象,用于记录队列统计信息,锁,队列状态,临时存储拉取的消息,相当于队列的状态对象
//messageQueue:记录了主题名、队列号、borker名字
ConsumeRequest consumeRequest = new ConsumeRequest(msgs, processQueue, messageQueue);
ConsumeRequest实现了Runnable接口,放入线程池后线程池执行其run方法。下面看一下run()方法的执行逻辑:
从上面的执行逻辑可以看出,监听器抛出异常或者返回RECONSUME_LATER,都属于消费失败,rocketmq对它们的处理是一样的。这里还有一点要注意,消费失败的请求对象与正常拉取消息的请求对象是不同的:
下面看一下ConsumerSendMsgBackRequestHeader的请求参数有哪些:
//设置消费组
requestHeader.setGroup(consumerGroup);
//设置原始主题
requestHeader.setOriginTopic(msg.getTopic());
//设置消费位移,这个消费位移是物理位移
requestHeader.setOffset(msg.getCommitLogOffset());
//设置重试策略,可以设置-1,0,大于0,默认是0,表示由broker控制
requestHeader.setDelayLevel(delayLevel);
//设置消息的id
requestHeader.setOriginMsgId(msg.getMsgId());
//设置重试次数,默认是16次
requestHeader.setMaxReconsumeTimes(maxConsumeRetryTimes);
ConsumerSendMsgBackRequestHeader请求发送到broker,下面来看broker如何处理该请求。
broker收到请求后,将请求转发给SendMessageProcessor.asyncConsumerSendMsgBack()方法。该方法首先进行校验,比如权限、是否有订阅组信息等。校验通过后,根据请求参数中的group值,也就是消费组,生成重试主题名:%RETRY%消费组名。默认情况下,重试主题有一个读队列和一个写队列。
之后根据请求参数中的位移,找到对应的消息,从消息中解析出当前的重试次数和消息体内容,如果是第一次重试,那么找到的消息其实就是原始消费失败的消息,自然得到的重试次数是0,之后将重试次数与允许的最大重试次数做比较:
从上面看到,重试消息没有写入到重试主题中,而是写入到了定时主题,也就是SCHEDULE_TOPIC_XXXX中。这是因为,消费者消费失败,一般需要延迟一段时间再重试,定时主题就是起这个作用,broker根据重试次数计算一个延迟级别:
if (0 == delayLevel) {
delayLevel = 3 + msgExt.getReconsumeTimes();//在重试次数的基础上+3
}
msgExt.setDelayTimeLevel(delayLevel);
延迟级别越大,延迟时间就越长。rocketmq定义了18个级别,延迟时间分别为(参见MessageStoreConfig.messageDelayLevel属性):
1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
broker上有一个服务ScheduleMessageService,该服务启动一个Timer定时器,定时读取每个定时队列里面的消息。
到了定时队列的读取时间后,rocketmq取出队列里面的消息,根据消息里面记录的真实队列和真实主题,创建一个新的消息,将消息写入到真实队列和真实主题中,其实也就是%RETRY%消费组名中。
通过前面两小节内容可以知道,消息消费失败了,消费者发送ConsumerSendMsgBackRequestHeader请求到broker,broker根据请求创建一个消息,并最终将消息写入重试队列。到这里,大家是否感到奇怪,broker为什么没有将消费失败的消息再返回给消费者,而是写入到了重试主题中?
这时候rocketmq的再平衡服务又要开始发挥作用了。在启动的时候,消费者根据消费组名生成对应的重试主题名,并将该主题加入到再平衡服务中,之后再平衡服务遍历主题,生成一个PullRequest拉取请求对象,PullMessageService服务发现PullRequest请求后,根据该请求创建PullMessageRequestHeader对象,向broker拉取重试主题的消息,拉取回后,对消息再次消费。
这里对前面三节的内容做一下总结:
之前提到重试次数超过16次,消息会被放入死信主题。
死信主题是用于存储一些无法成功消费的消息,当消息重试次数超过最大重试次数,就会被放入该主题中。
SendMessageProcessor.asyncConsumerSendMsgBack()每次收到消息失败的消息后,都会根据消费位移取出消息,得到消息的重试次数,将该次数与最大重试次数比较:
//如果超过了最大重试次数,将消息放入死信主题
//maxReconsumeTimes表示最大重试次数
if (msgExt.getReconsumeTimes() >= maxReconsumeTimes
|| delayLevel < 0) {
//生成死信主题名:%DLQ%消费组名
newTopic = MixAll.getDLQTopic(requestHeader.getGroup());
//死信主题只有一个队列
queueIdInt = Math.abs(this.random.nextInt() % 99999999) % DLQ_NUMS_PER_GROUP;
//创建死信主题配置对象,如果没有死信主题,则创建
topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(newTopic,
DLQ_NUMS_PER_GROUP,
PermName.PERM_WRITE, 0);
//如果死信主题创建失败,返回请求方失败信息
if (null == topicConfig) {
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark("topic[" + newTopic + "] not exist");
return CompletableFuture.completedFuture(response);
}
}
之后broker将创建一个消息,并将该消息写入到死信主题中,其中消息的消息体为消费失败的消息体。
死信主题中的消息,默认情况下是不会自动消费的,需要开发人员编写程序读取其中的消息或者从rocketmq提供监控台上查看。