
阿里二面:RocketMQ 消费失败了,怎么处理?
大家好,我是君哥。今天来聊一聊 RocketMQ 客户端消息消费失败,怎么办?
下面是 RocketMQ 推模式的一段代码:
从这段代码可以看出,消费者消费消息后会返回一个消费状态,那消费状态有哪些呢?参见类 ConsumeConcurrentlyStatus 中定义:
- 消费成功,返回 CONSUME_SUCCESS;
- 消费失败,返回 RECONSUME_LATER。
下面代码就是返回上面两个状态的逻辑,对于消费状态,如果返回 null,会给它赋值 RECONSUME_LATER,处理逻辑如下:
这部分代码的 UML 类图如下:
上面代码中的 processConsumeResult 方法就是消费失败后客户端的处理逻辑:
1 消费成功
上面的代码逻辑中,如果消费成功,ackIndex 变量的值就是消息数量减 1,所以上面的 switch 逻辑是不会执行的,因为广播模式下,只是打印一段日志(没有其他逻辑),而集群模式下,for 循环的起始 i 变量已经等于消息数量,循环里面的代码不会执行。
因此,如果消息消费成功,只会走最下面的逻辑,更新本地保存的消息偏移量。
2 消费失败
ackIndex 变量值等于 -1。
2.1 广播模式
在消费失败的情况下,广播模式的代码只是打印了一段日志,之后更新了本地保存的消息偏移量,因此我们知道广播模式消息消费失败后就不会重新消费了,相当于丢弃了消息。
2.2 集群模式
从上面代码的 for 循环中,会把所有的消息都发送回 Broker,这样这批消息还能再次被拉取到进行消费。
对于发送给 Broker 失败的消息,会延迟 5s 后再次消费。代码如下:
更新本地保存的消息偏移量时,会从消息列表中把发送回 Broker 失败的消息先删除掉。
注意:从上面逻辑可以看到,在拉取到一批消息进行消费时,只要有一条消息消费失败,这批消息都会进行重试,因此消费端做好幂等是必要的。
下面再看一下发送失败消息给 Broker 的代码,发送消息是,请求的 code 码是 CONSUMER_SEND_MSG_BACK。根据这个请求码就能找 Broker 端的处理逻辑。
如果发送回 Broker 时抛出异常,需要重新发送一个新的消息,这里有四点需要注意:
- 新消息的 Topic 变成【 %RETRY% + consumerGroup】;
- 新消息的 RETRY_TOPIC 这个属性赋值为之前的 Topic;
- 新消息的重试次数属性加 1;
- 新消息的 DELAY 属性等于重试次数 + 3.
2.3 Broker 处理
上面已经讲过,对于处理失败的消息,消费端会发送回 Broker,不过这里有一点需要注意,发送回 Broker 时,消息的 Topic 变成【"%RETRY%" + namespace + "%" + 原始 topic】,封装逻辑在源码 ClientConfig.withNamespace。
根据请求码 CONSUMER_SEND_MSG_BACK 可以定位到 Broker 的处理逻辑在类 SendMessageProcessor,方法 asyncConsumerSendMsgBack。
2.3.1 进死信队列
如果重试次数超过了最大重试次数(默认 16 次),或者 delayLevel 值小于0,则消息进死信队列,死信队列的 Topic 为【"%DLQ%" + 消费组】,代码如下:
2.3.2 发送 CommitLog
如果延迟级别(DELAY)等于 0,则延迟级别就等于重试次数加 3。
有个地方需要注意,发送到延迟队列的消息重新进行了封装,封装这个消息用的并不是客户端发来的那个消息,而是从 CommitLog 中根据偏移量查找的,代码如下:
如果查询失败,就会给客户端返回系统错误。
这里有个重要的细节,这个消息写入 CommitLog 时,会判断 DELAY 是否大于 0,如果大于 0,就会修改 Topic。代码如下:
这里把 Topic 修改为 SCHEDULE_TOPIC_XXXX,供延时队列来调度。进入延时队列后,延时队列会按照下面的时间进行调度:
上面代码可以看到,延时消息的调度有 18 个等级,最小的 1s,最大的 2h。而从下面的代码我们可以看到,调度使用第三个等级开始的:
2.3.3 延时队列
延时队列的代码逻辑在类 ScheduleMessageService,这里的 start 方法触发延时队列的调度,而 start 方法的业务入口在 BrokerStartup 的初始化。
首先,会计算出每个延时等级对应的延时时间(处理到 ms 级别),放到 delayLevelTable,它是一个 ConcurrentHashMap,然后创建一个核心线程数等于 18 的定时线程池,依次对每个级别的延时进行调度。这个任务启动后,会每 100ms 执行一次。代码如下:
调度逻辑中,首先根据 Topic 和 queueId 找到对应的消费队列,然后从里面连续读取消息:
因为 messageTimeup 方法使用了原始的 Topic 和 QueueId 新建了消息,所以上面的 syncDeliver 方式是将消息重新投递到原始的队列中,这样消费者可以再次拉取到这条消息进行消费。注意:上面 ConsumeQueue 的 tagsCode 是一个时间点,很容易误解为是 tag 的 hashCode,MessageQueue 的存储元素中最后 8 字节确实是 tag 的 hashCode。
3 总结
消费者消费失败后,会把消费发回给 Broker 进行处理。下图是客户端处理流程:
Broker 收到消息后,会把消息重新发送到 CommitLog,发送到 CommitLog 之前,首先会修改 Topic 为 SCHEDULE_TOPIC_XXXX,这样就发送到了延时队列,延时队列再根据延时级别把消息投递到原始的队列,这样消费者就能再次拉取到。流程如下图:
从流程来看,消费者批量拉取消息,如果部分消息消费失败,那就会整批全部重试。所以做好幂等是必要的。
文章转载自公众号:君哥聊技术
