主要关注业务方在消息消费失败后,返回 ConsumeConcurrentlyStatus.RECONSUME_LATER ,专业术语:业务方每条消息消费后要告诉 MQ 消费者一个结果(ack,message back),触发 MQ 消息消费重试机制,然后 MQ 消费者需要反馈给 MQ(Broker)。
备注:主要针对的还是非顺序消息,顺序消息在后续专题详细分析。
代码入口:ConsumeMessageConcurrentlyService ConsumeRequest run方法
然后进入到结果处理:ConsumeMessageConcurrentlyService processConsumeResult
如果返回结果是 CONSUME_SUCCESS,此时 ackIndex = msg.size() - 1,再看发送 sendMessageBack 循环的条件,for (int i = ackIndex + 1; i < msg.size() ;;) 从这里可以看出如果消息成功,则无需发送sendMsgBack 给 broker。
如果返回结果是 RECONSUME_LATER, 此时 ackIndex = -1 ,则这批所有的消息都会发送消息给Broker,也就是这一批消息都得重新消费。如果发送 ack 失败,则会延迟5s后重新在消费端重新消费。
消费者向 Broker 发送 ACK 消息,如果发送成功,重试机制由 broker 处理,如果发送 ack 消息失败,则将该任务直接在消费者这边,再次在本地处理该批消息,默认演出5s后在消费者重新消费,其关键总结如下:
然后我们重点跟踪 sendMessageBack 方法:
DefaultMQPushConsumerImpl sendMessageBack
核心实现要点如下:
SendMessageProcessor processRequest
最近在 Broker 端会处理 CONSUMER_SEND_MSG_BACK命令。
其代码入口:SendMessageProcessor#consumerSendMsgBack。
其核心熟悉解释如下:
SendMessageProcessor#consumerSendMsgBack
TopicConfigManager #createTopicInSendMessageBackMethod
如果创建主题配置信息错误,会抛出系统异常,产生的效果是消费端发送ACK消息错误,会创建一条新的消息,消息内部ID为原消息ID,然后重新发送给Broker。
SendMessageProcessor#consumerSendMsgBack
2.4、延迟级别、消费次数处理
SendMessageProcessor#consumerSendMsgBack
如果消息次数或延迟级别小于0,设置消息的主题为 DLQ+ 消费组名称,如果消息的延迟级别为0,则 3 + 消息重试的次数。
如果消息发送成功,则返回成功,否则返回错误,消费端会将这些消息直接在消费端延迟5S后重新消费。
现在成功将消息发送到 commitlog 中,主题为 RETRY_TOPIC + 消费组名称,,也就是消息重试的消息主题是基于消费组。而不是每一个主题都有一个重试主题。而是每一个消费组由一个重试主题。那这些主题的消息,又是如何在被消费者获取并进行消费的。
然后进行消费进度更新:
进度更新,本文不深入学习,后续会专门研究消费进度保持机制。
目前,重试机制的前半部分已经讲解完成,再次复习一下:
消息现在是存储到 commitlog 文件里了,那怎么消费呢。
通篇搜索 DelayLevel,一个比较关键的类 org.apache.rocketmq.store.schedule.ScheduleMessageService 映入眼帘,稍微浏览一下,就知道该类与延迟类消息息息相关,但是处理的主题却是 SCHEDULE_TOPIC = "SCHEDULE_TOPIC_XXXX",我们延迟消息的主题却是RETRY + 消费组名称,主题不一样呀,得继续找,继续全文搜索 delayLevel,发现 CommitLog 类的 putMessage中竟然也出现了 delayLevel 相关的处理,我们重点观察一下该代码:org.apache.rocketmq.store.CommitLog#putMessage
注意,在消息存入commitlog之前,如果发现延迟level大于0,会将消息的主题设置为SCHEDULE_TOPIC = "SCHEDULE_TOPIC_XXXX",然后备份原主题名称。那就清晰明了,延迟消息统一由 ScheduleMessageService 来处理。
ScheduleMessageService 的源码我就不一一分析了,从此类可以得出如下结论:关于RocketMQ 延迟消息机制:
到这里,我们弄清楚了消息重试,消息的流转,但还是没有找到 RETRY+消费组(队列的订阅信息)。
那消费者是如何订阅RETRY+消费组名称 的消费队列的呢?
原来在消费者启动时,就默认会订阅该消费组的重试主题的队列。
org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl
那一切关于RocketMQ消息重试机制的谜底就一一揭晓了。
下面对消息消费重试做一个简单的总结:
1、如果返回结果是 CONSUME_SUCCESS,此时 ackIndex = msg.size() - 1, 再看发送 sendMessageBack 循环的条件,for (int i = ackIndex + 1; i < msg.size() ;;) 从这里可以看出如果消息成功,则无需发送 sendMsgBack 给 broker;如果返回结果是RECONSUME_LATER, 此时 ackIndex = -1 ,则这批所有的消息都会发送消息给 Broker,也就是这一批消息都得重新消费。
如果发送ack消息失败,则会延迟5s后重新在消费端重新消费。
首先消费者向 Broker 发送 ACK 消息,如果发生成功,重试机制由 broker 处理,如果发送 ack 消息失败,则将该任务直接在消费者这边,再次将本次消费任务,默认演出5S后在消费者重新消费。
2、需要延迟执行的消息,在存入 commitlog 之前,会备份原先的主题(retry+消费组名称)、与消费队列ID,然后将主题修改为SCHEDULE_TOPIC_XXXX,会被延迟任务 ScheduleMessageService 延迟拉取。
3、ScheduleMessageService 在执行过程中,会再次存入 commitlog 文件中放入之前,会清空延迟等级,并恢复主题与队列,这样,就能被消费者所消费,因为消费者在启动时就订阅了该消费组的重试主题。
备注:本文是《RocketMQ技术内幕》的前期素材,建议关注笔者的书籍:《RocketMQ技术内幕》。
欢迎加笔者微信号(dingwpmz),加群探讨,笔者优质专栏目录:
1、源码分析RocketMQ专栏(40篇+)
2、源码分析Sentinel专栏(12篇+)
3、源码分析Dubbo专栏(28篇+)
4、源码分析Mybatis专栏
5、源码分析Netty专栏(18篇+)
6、源码分析JUC专栏
7、源码分析Elasticjob专栏
8、Elasticsearch专栏
9、源码分析Mycat专栏