消息重发、重试消费、死信队列

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:
posted @   QiaoZhi  阅读(758)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
历史上的今天:
2019-03-28 EmbeddedSolrServer的使用与solor6.3.0的使用
2018-03-28 linux系统引导流程
2018-03-28 CentOS7修改默认运行级别
2018-03-28 关于Linux下s、t、i、a权限
点击右上角即可分享
微信分享提示