消息重发、重试消费、死信队列
1. 消息发送重试机制
1. 简介
producer对发送失败的消息进行重新发送的机制,称为消息发送重试机制,也称为消息重投机制。
有一些限制:
- 生产者在发送消息时,若采用同步或异步发送方式,发送失败会重试,但oneway 消息发送方式发送失败是没有重试机制的。
- 只有普通消息有重试,顺序消息没有重试
- 消息重投机制会造成消费消息重复消费。一般不会发送消息重复,在出现消息量大、网络抖动,消息重复就成为大概率事件。producer主动重发、consumer负载变化(发生Rebalance,不会导致消息重复,但可能出现重复消费)也会导致重复消息。消息重复无法避免,需要避免消息的重复消费
- 避免消息重复消费的解决方案:为消息添加唯一标识(例如消息key),使消费者进行判断
- 消息发送重试有三种策略可以选择:同步发送失败策略、异步发送失败策略、消息刷盘失败策略
2. 同步发送失败策略
对于普通消息,消息发送默认采用轮询策略来选择发送到的队列。如果发送失败,默认重试2次。在重试时如果有其他broker的时候会选择其他broker;当只有一个broker的时候也只能发送到该broker,会尽量选择该Broker的其他Queue。同时,broker还具有失败隔离功能,使producer尽量选择未失败的Broker 作为目标Broker。超过重试次数,则抛出异常,由producer 去保证消息不丢失。源代码如下:
org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendDefaultImpl
private SendResult sendDefaultImpl( Message msg, final CommunicationMode communicationMode, final SendCallback sendCallback, final long timeout ) throws MQClientException, RemotingException, MQBrokerException, InterruptedException { this.makeSureStateOK(); Validators.checkMessage(msg, this.defaultMQProducer); final long invokeID = random.nextLong(); long beginTimestampFirst = System.currentTimeMillis(); long beginTimestampPrev = beginTimestampFirst; long endTimestamp = beginTimestampFirst; TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic()); if (topicPublishInfo != null && topicPublishInfo.ok()) { boolean callTimeout = false; MessageQueue mq = null; Exception exception = null; SendResult sendResult = null; // 默认次数是重试次数2次 + 1次发送次数 int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1; int times = 0; String[] brokersSent = new String[timesTotal]; for (; times < timesTotal; times++) { String lastBrokerName = null == mq ? null : mq.getBrokerName(); MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName); if (mqSelected != null) { mq = mqSelected; brokersSent[times] = mq.getBrokerName(); try { beginTimestampPrev = System.currentTimeMillis(); if (times > 0) { //Reset topic with namespace during resend. msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic())); } long costTime = beginTimestampPrev - beginTimestampFirst; if (timeout < costTime) { callTimeout = true; break; } sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime); endTimestamp = System.currentTimeMillis(); this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false); switch (communicationMode) { ......
producer也可以修改一些默认参数:
DefaultMQProducer producer = new DefaultMQProducer("syncProducer"); producer.setNamesrvAddr("192.168.13.111:9876"); // 设置发送失败的重试次数,默认是2次 producer.setRetryTimesWhenSendFailed(4); // 设置消息发送超时时长是5s,默认是3s producer.setSendMsgTimeout(5 * 1000);
3. 异步发送失败策略
异步失败发送失败时,异步重试不会选择其他broker,仅在一个broker 做重试,所以该策略无法保证消息不丢失。
DefaultMQProducer producer = new DefaultMQProducer("asyncProducer"); producer.setNamesrvAddr("192.168.13.111:9876"); // 指定异步发送失败后不进行消息重试 producer.setRetryTimesWhenSendAsyncFailed(0); producer.start();
4. 消息刷盘失败策略
消息刷盘超时(master或者slave)或slave不可用时,默认是不会将消息尝试发送到其他broker的。可以在配置文件设置 restryAnotherBrokerWhenNotStoreOK=true 来开启。
2. 消息消费重试机制
1. 顺序消息消费重试
为了保证顺序消息的顺序性,消费失败后会自动不断地进行消息重试,直到消费成功。消费重试,默认间隔时间为1000ms。重试期间应该会出现消费被阻塞的情况。
注意: 顺序消息没有发送重试,但是有消费重试。对于顺序消息的消费,要注意其一直重复消费,避免永久性阻塞。
2. 无序消息消费重试
(1) 简介
对于无序消息(普通消息、延时消息、事务消息),可以通过设置返回状态达到消息重试的效果。不过,需要注意的是,无序消息的重试只对集群消费方式生效。也就是广播消费模式下,消费失败的消息没有重试。
(2) 重试次数与间隔
默认最多重试16次,每次就间隔不同,会逐渐变长。重试完之后仍然失败,消息会投递到死信队列。其消息重试时间如下:
10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
也可以设置消费者消息的最大重试次数:
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("myTestConsumerGroup"); /** * 默认时间: 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h * 修改后时间规则: 如果次数小于十六,按原来时间执行; 超过16 每次都是2小时 * 对于ConsumerGroup, 修改一个会影响当前consumerGroup的所有实例,采用覆盖的方式以最后一次修改为准(因为规则跑在mq服务端) */ consumer.setMaxReconsumeTimes(10);
(3) 简单理解
对于需要重试消费的消息,是将这些需要重试的消息放入到了一个特殊Topic的队列中,这个队列就是重试队列。
当出现需要进行重试消费时,broker会为每个消费组都设置一个名称为%RETRY%consumerGroupName的重试队列。
这个重试队列是为消费者组设置的,而不是topic(一个topic可以被多个组进行消费)
只有当出现重试消费的消息时,才会为该组创建重试队列
测试如下:
[root@redisnode01 consumequeue]# pwd /root/store/consumequeue [root@redisnode01 consumequeue]# ll | grep myTest drwxr-xr-x. 3 root root 15 Mar 24 08:18 %DLQ%myTestConsumerGroup drwxr-xr-x. 3 root root 15 Mar 22 22:39 %RETRY%myTestConsumerGroup
也可以通过偏移量文件进行查看:(/root/store/config/consumerOffset.json)
{ "offsetTable":{ "syncTopic@LitePullConsumer":{0:115,1:118,2:117,3:115 }, "dlqTopic@myTestConsumerGroup2":{0:5,1:7,2:6,3:7 }, "txTopic@myTestConsumerGroup":{0:4,1:3,2:4,3:4 }, "syncTopic@myTestConsumerGroup":{0:145,1:146,2:145,3:144 }, "%RETRY%myTestConsumerGroup@myTestConsumerGroup":{0:335 }, "syncTopic@myTestConsumerGroup2":{0:145,1:146,2:145,3:144 }, "%RETRY%myTestConsumerGroup2@myTestConsumerGroup2":{0:75 }, "RMQ_SYS_TRANS_HALF_TOPIC@CID_RMQ_SYS_TRANS":{0:48 }, "dlqTopic@myTestConsumerGroup":{0:4,1:6,2:5,3:5 }, "RMQ_SYS_TRANS_OP_HALF_TOPIC@CID_RMQ_SYS_TRANS":{0:32 }, "batchTest@myTestConsumerGroup":{0:145567,1:176453,2:169162,3:122128 }, "filterTopic@myTestConsumerGroup":{0:22,1:23,2:17,3:18 } } }
(4) 实现原理
从上面可以看出消息重试的时间间隔与延迟消息的延迟等级十分相似(除了没有延迟消息的前两个等级)。broker对于重试消息的处理是通过延时消息实现的。先将消息按保存到主題 SCHEDULE_TOPIC_XXXX 的队列中(根据等级对应选择队列),延迟时间到了后会将消息重新投递到主题为%RETRY%consumerGroupName 的队列中。可以查看SCHEDULE_TOPIC_XXXX 队列目录:
[root@redisnode01 consumequeue]# pwd /root/store/consumequeue [root@redisnode01 consumequeue]# ls SCHEDULE_TOPIC_XXXX/ 0 10 11 12 13 14 15 16 17 2 3 4 5 6 7 8 9
3. 消息重试返回码
集群消费模式下监听器重复消费如下:
- 返回org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus#RECONSUME_LATER(建议这种)
- 抛出异常
- 返回null
集群消费模式下监听器取消重复消费:
- 自己try...catch 直接返回 org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus#CONSUME_SUCCESS
3. 死信队列
1. 简介
一条消息达到指定的重试次数之后,依然消费失败,则消息进入一个特殊的队列。这个队列就是死信队列(Dead-Letter Queue),里面的消息称为死信消息。死信队列是针对消费者组的,其名称为%DLQ%consumerGroupName。比如:
[root@redisnode01 consumequeue]# ll | grep myTest drwxr-xr-x. 3 root root 15 Mar 24 08:18 %DLQ%myTestConsumerGroup drwxr-xr-x. 3 root root 15 Mar 22 22:39 %RETRY%myTestConsumerGroup drwxr-xr-x. 3 root root 15 Mar 28 08:44 %RETRY%myTestConsumerGroup2 [root@redisnode01 consumequeue]# ls %DLQ%myTestConsumerGroup/ 0
2. 死信队列特征
- 死信队列的消息不会被消费者正常消费,即DLQ对于消费者不可见
- 死信存储有效期与正常消息一样,均为3天,3天后会被自动删除
- 死信队列其实就是一个特殊的topic,名称为%DLQ%consumerGroupName, 也就是每个消费者组都有一个死信队列
- 如果一个消费者组未产生死信消息,不会为其创建该topic
3. 测试
如果想一个消息进入死信队列,可以指定消息重复消费次数,然后返回非正常的状态码:
package com.zd.bx.rocketmq.dlq; import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.message.MessageExt; import java.util.List; public class PushConsumer { public static void main(String[] args) throws InterruptedException, MQClientException { DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("myTestConsumerGroup3"); /** * 默认时间: 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h * 修改后时间规则: 如果次数小于十六,按原来时间执行; 超过16 每次都是2小时 * 对于ConsumerGroup, 修改一个会影响当前consumerGroup的所有实例,采用覆盖的方式以最后一次修改为准(因为规则跑在mq服务端) */ consumer.setMaxReconsumeTimes(2); // 设置线程数量 consumer.setConsumeThreadMax(4); consumer.setConsumeThreadMin(2); // 指定nameserver consumer.setNamesrvAddr("192.168.13.111:9876"); // 指定消费的topic与tag consumer.subscribe("dlqTopic", "*"); consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { for (MessageExt msg : msgs) { System.out.printf("%s Receive New Messages, body: %s %n", Thread.currentThread().getName(), new String(msg.getBody())); } return null; } }); consumer.start(); System.out.printf("Consumer Started.%n"); } }
服务器查看其主题下队列:
[root@redisnode01 consumequeue]# ls -R | grep myTestConsumerGroup3 %DLQ%myTestConsumerGroup3 %RETRY%myTestConsumerGroup3 ./%DLQ%myTestConsumerGroup3: ./%DLQ%myTestConsumerGroup3/0: ./%RETRY%myTestConsumerGroup3: ./%RETRY%myTestConsumerGroup3/0:
注意
发送端
- 如果同步模式发送失败,则轮转到下一个Broker,如果异步模式发送失败,则只会在当前Broker进行重试。
- 发送消息超时时间默认3000毫秒,如果因为超时,那么便不再尝试重试。
- 发送消息失败会尝试重试,当达到最大重试次数后才会抛出异常,可以通过捕获异常,来添加日志数据库,通过定时任务进行补偿机制。
消费端
Consumer消费消息失败后,要提供一种重试机制,令消息至少再消费一次。通常引起消息消费重试的时候包括两种情况:异常重试和超时重试。另外,Consumer在广播模式下重试失效。
异常重试:(只要没有返回SUCCESS状态)
上面讲的消费者重试都是异常重试;
1、并行消费的重试策略,即MessageListener为MessageListenerConcurrently:
SubscriptionGroupConfig类的配置中,默认最大重试16次(retryMaxTimes属性,默认从level3开始),但如果Consumer端逻辑出现异常,重试太多次也没有很大的意义,因此我们可以在代码中指定最大的重试次数,达到一定次数之后就返回SUCCESS,不再重试,对于失败的消息记录到数据库的表中,后续人工处理。
2、串行消费策略,即MessageListener为MessageListenerOrderly:
将会暂停后续消费进度,对于该消息无限的进行重试投递(Integer.MAX),这是需要特别注意的。所以应该设置最大重试次数,达到后进行记录日志。
3、可通过consumer 客户端参数MaxReconsumeTimes设置最大重试次数,超过最大重试次数,消息将被转移到死信队列,范围是-1 – 16之间。对于有序消费模式MessageListenerOrderly,默认-1,表示无限次本地立即重试消费,间隔时间可通过自定义参数suspendCurrentQueueTimeMillis取值进行配置。参数取值范围:10~30000,单位:毫秒,默认值:1000毫秒,即1秒。对于并发无序消费模式MessageListenerConcurrently,默认16次延时消费,从Level3开始。
超时重试:
如果Consumer端处理时间过长,或者由于某些原因线程挂起,导致迟迟没有返回任何消费状态(成功或者失败),Broker就会认为Consumer消费超时,此时会发起超时重试,默认的消费超时时间为15分钟,时间足够的长了。
RocketMQ会认为该消息没有发送到Consumer端,会一直不停的发送,即无限重试。
转载:https://www.cnblogs.com/qlqwjy/p/16064992.html
https://blog.csdn.net/weixin_43767015/article/details/121135114