SpringBoot整合Rabbitmq之死信队列

目录

  • 1. 死信队列
    • 1.1. 死信从何而来
    • 1.2. 死信流程
  • 2. 死信队列实现
    • 2.1. 消息发送方
      • 2.1.1. 配置文件
      • 2.1.2. 队列、交换机定义及绑定
      • 2.1.3. `Constant` 类
      • 2.1.4. 消息发送确认机制
      • 2.1.5. 消息发送类
    • 2.2. 消息接收方
      • 2.2.1. 配置文件
      • 2.2.2. 业务消息消费
      • 2.2.3. 死信消息消费
    • 2.3. 测试
      • 测试(`1`)
      • 测试(`2`)
      • 测试(`3`)
  • 3. 死信队列的应用场景
  • 4. 小结

1. 死信队列

死信:无法被消费方消费掉的消息,称为死信。如果死信一直留在队列中,会导致一直被消费,却从不消费成功。所以 rabbitmq 专门开辟了一个来存放死信的队列,叫死信队列(DLX,dead-letter-exchange

1.1. 死信从何而来

  • 消息消费方调用了 basicNack()basicReject(),并且参数都是 requeue = false,则消息会路由进死信队列
  • 消息过期,过了 TTL 存活时间,就是消费方在 TTL 时间之内没有消费,则消息会路由进死信队列
  • 队列设置了 x-max-length 最大消息数量且当前队列中的消息已经达到了这个数量,再次投递,消息将被挤掉,被挤掉的消息会路由进死信队列
  • 有一种场景需要注意下:消费者设置了自动 ACK,当重复投递次数达到了设置的最大 retry 次数之后,消息也会投递到死信队列,但是内部的原理还是调用了 basicNack()basicReject()
#开启rabbitmq的生产端重试机制,默认是false,默认重试 3 次
spring.rabbitmq.template.retry.enabled=true
#开启rabbitmq的消费端重试机制,默认是false,默认重试 3 次
spring.rabbitmq.listener.simple.retry.enabled=true
#设置重试的次数
spring.rabbitmq.listener.simple.retry.max-attempts=5

1.2. 死信流程

死信流程图

SpringBoot整合Rabbitmq之死信队列_第1张图片

  • 正常业务消息被投递到正常业务的 Exchange,该 Exchange 根据路由键将消息路由到绑定的正常队列
  • 正常业务队列中的消息变成了死信消息之后,会被自动投递到该队列绑定的死信交换机上(并带上配置的路由键,如果没有指定死信消息的路由键,则默认继承该消息在正常业务时设定的路由键)
  • 死信交换机收到消息后,将消息根据路由规则路由到指定的死信队列
  • 消息到达死信队列后,可监听该死信队列,处理死信消息

死信队列并不是什么特殊的队列,只不过是绑定在死信交换机上的队列。死信交换机也不是什么特殊的交换机,只不过是用来接收死信的交换机,所以可以为任何类型(Direct、Fanout、Topic。一般来说,会为每个业务队列分配一个独有的路由 key,并对应的配置一个死信队列进行监听,也就是说,一般会为 每个重要的业务队列配置一个死信队列

2. 死信队列实现

  • SpringBoot 版本 2.0.6.RELEASE
  • Rabbitmq 版本 3.8.3

分别创建项目消息生产方,消息接收方两个项目

2.1. 消息发送方

2.1.1. 配置文件

server.port=8080

#配置rabbitmq服务器
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
#确认消息已发送到交换机
spring.rabbitmq.publisher-confirms=true
#确认消息已发送到队列
spring.rabbitmq.publisher-returns=true

2.1.2. 队列、交换机定义及绑定

@Configuration
public class DeadLetterConfig {

    // --------------------------正常业务队列--------------------------
    // 业务队列 A
    @Bean
    public Queue businessQueueA() {
        Map<String, Object> args = new HashMap<>();
        // x-dead-letter-exchange:这里声明当前业务队列绑定的死信交换机
        args.put("x-dead-letter-exchange", Constant.DEAD_LETTER_EXCHANGE);
        // x-dead-letter-routing-key:这里声明当前业务队列的死信路由 key
        args.put("x-dead-letter-routing-key", Constant.DEAD_LETTER_QUEUE_A_ROUTING_KEY);
        return new Queue(Constant.BUSINESS_QUEUE_A, true, false, false, args);
    }

    // 业务队列 B
    @Bean
    public Queue businessQueueB() {
        Map<String, Object> args = new HashMap<>();
        // x-dead-letter-exchange:这里声明当前队列绑定的死信交换机
        args.put("x-dead-letter-exchange", Constant.DEAD_LETTER_EXCHANGE);
        // x-dead-letter-routing-key:这里声明当前队列的死信路由 key
        args.put("x-dead-letter-routing-key", Constant.DEAD_LETTER_QUEUE_B_ROUTING_KEY);
        return new Queue(Constant.BUSINESS_QUEUE_B, true, false, false, args);
    }

    // 业务队列的交换机
    @Bean
    public TopicExchange businessTopicExchange() {
        return new TopicExchange(Constant.BUSINESS_EXCHANGE, true, false);
    }

    // 业务队列 A 与交换机绑定,并指定 Routing_Key
    @Bean
    public Binding businessBindingA() {
        return BindingBuilder.bind(businessQueueA()).to(businessTopicExchange()).with(Constant.BUSINESS_QUEUE_A_ROUTING_KEY);
    }

    // 业务队列 B 与交换机绑定,并指定 Routing_Key
    @Bean
    public Binding businessBindingB() {
        return BindingBuilder.bind(businessQueueB()).to(businessTopicExchange()).with(Constant.BUSINESS_QUEUE_B_ROUTING_KEY);
    }

    // --------------------------死信队列--------------------------
    // 死信队列 A
    @Bean
    public Queue deadLetterQueueA() {
        return new Queue(Constant.DEAD_LETTER_QUEUE_A);
    }

    // 死信队列 B
    @Bean
    public Queue deadLetterQueueB() {
        return new Queue(Constant.DEAD_LETTER_QUEUE_B);
    }

    // 死信交换机
    @Bean
    public DirectExchange deadLetterDirectExchange() {
        return new DirectExchange(Constant.DEAD_LETTER_EXCHANGE);
    }

    // 死信队列 A 与死信交换机绑定,并指定 Routing_Key
    @Bean
    public Binding deadLetterBindingA() {
        return BindingBuilder.bind(deadLetterQueueA()).to(deadLetterDirectExchange()).with(Constant.DEAD_LETTER_QUEUE_A_ROUTING_KEY);
    }

    // 死信队列 B 与死信交换机绑定,并指定 Routing_Key
    @Bean
    public Binding deadLetterBindingB() {
        return BindingBuilder.bind(deadLetterQueueB()).to(deadLetterDirectExchange()).with(Constant.DEAD_LETTER_QUEUE_B_ROUTING_KEY);
    }

    // --------------------------使用 RabbitAdmin 启动服务便创建交换机和队列--------------------------
    @Bean
    public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {
        RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);
        // 只有设置为 true,spring 才会加载 RabbitAdmin 这个类
        rabbitAdmin.setAutoStartup(true);
        // 创建死信交换机和对列
        rabbitAdmin.declareExchange(deadLetterDirectExchange());
        rabbitAdmin.declareQueue(deadLetterQueueA());
        rabbitAdmin.declareQueue(deadLetterQueueB());
        // 创建业务交换机和对列
        rabbitAdmin.declareExchange(businessTopicExchange());
        rabbitAdmin.declareQueue(businessQueueA());
        rabbitAdmin.declareQueue(businessQueueB());
        return rabbitAdmin;
    }
}
  • 两个业务队列 AB 分别与 TopicExchange 类型的业务交换机绑定,指定路由 key
  • 两个业务队列 AB 分别与 DirectExchange 类型的 死信 交换机绑定,指定路由 key(一旦成为死信,通过该 key 路由至死信队列中),代码如下
Map<String, Object> args = new HashMap<>();
// x-dead-letter-exchange:这里声明当前业务队列绑定的死信交换机
args.put("x-dead-letter-exchange", Constant.DEAD_LETTER_EXCHANGE);
// x-dead-letter-routing-key:这里声明当前业务队列的死信路由 key
args.put("x-dead-letter-routing-key", Constant.DEAD_LETTER_QUEUE_A_ROUTING_KEY);
  • 两个死信队列 AB 分别与 DirectExchange 类型的死信交换机绑定,指定路由 key

arguments 具体参数如下

参数名 作用
x-message-ttl 发送到队列的消息在丢弃之前可以存活多长时间(毫秒)
x-max-length 队列最大长度
x-expires 队列在被自动删除(毫秒)之前可以使用多长时间
x-max-length-bytes 队列在开始从头部删除之前可以包含的就绪消息的总体大小
x-dead-letter-exchange 设置队列溢出行为。这决定了在达到队列的最大长度时消息会发生什么。
有效值为drop-head或reject-publish。交换的可选名称,如果消息被拒绝或过期,将重新发布这些名称
x-dead-letter-routing-key 可选的替换路由密钥,用于在消息以字母为单位时使用。如果未设置,将使用消息的原始路由密钥
x-max-priority 队列支持的最大优先级数;如果未设置,队列将不支持消息优先级
x-queue-mode 将队列设置为延迟模式,在磁盘上保留尽可能多的消息以减少内存使用;如果未设置,队列将保留内存缓存以尽快传递消息
x-queue-master-locator 将队列设置为主位置模式,确定在节点集群上声明时队列主机所在的规则
x-overflow 队列达到最大长度时,可选模式包括: drop-head, reject-publishreject-publish-dlx.

2.1.3. Constant

public class Constant {

    public static final String BUSINESS_EXCHANGE = "dead.letter.business.exchange";
    public static final String BUSINESS_QUEUE_A = "dead.letter.business.queuea";
    public static final String BUSINESS_QUEUE_B = "dead.letter.business.queueb";
    public static final String BUSINESS_QUEUE_A_ROUTING_KEY = "dead.letter.business.queuea";
    public static final String BUSINESS_QUEUE_B_ROUTING_KEY = "dead.letter.business.#";

    public static final String DEAD_LETTER_EXCHANGE = "dead.letter.deadletter.exchange";
    public static final String DEAD_LETTER_QUEUE_A_ROUTING_KEY = "dead.letter.deadletter.queuea";
    public static final String DEAD_LETTER_QUEUE_B_ROUTING_KEY = "dead.letter.deadletter.queueb";
    public static final String DEAD_LETTER_QUEUE_A = "dead.letter.deadletter.queuea";
    public static final String DEAD_LETTER_QUEUE_B = "dead.letter.deadletter.queueb";
}

2.1.4. 消息发送确认机制

@Slf4j
@Configuration
public class RabbitConfig {

    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate();
        rabbitTemplate.setConnectionFactory(connectionFactory);
        /*设置开启Mandatory才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数*/
        rabbitTemplate.setMandatory(true);

        /*消息发送到Exchange的回调,无论成功与否*/
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            log.info("ConfirmCallback:" + "相关数据:" + correlationData);
            log.info("ConfirmCallback:" + "确认情况:" + ack);
            log.info("ConfirmCallback:" + "原因:" + cause);
        });

        /*消息从Exchange路由到Queue失败的回调*/
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
            log.info("ReturnCallback:" + "消息:" + message);
            log.info("ReturnCallback:" + "回应码:" + replyCode);
            log.info("ReturnCallback:" + "回应信息:" + replyText);
            log.info("ReturnCallback:" + "交换机:" + exchange);
            log.info("ReturnCallback:" + "路由键:" + routingKey);
        });
        return rabbitTemplate;
    }
}

2.1.5. 消息发送类

@Controller
public class SendMessageController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

	// 死信队列接口
    @GetMapping("/sendDeadLetterMessage1")
    @ResponseBody
    public String sendDeadLetterMessage1() {
        String messageId = String.valueOf(UUID.randomUUID());
        String messageData = "message: dead.letter.business.exchange test message ";
        String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        Map<String, Object> map = new HashMap<>();
        map.put("messageId", messageId);
        map.put("messageData", messageData);
        map.put("createTime", createTime);
        rabbitTemplate.convertAndSend(Constant.BUSINESS_EXCHANGE, Constant.BUSINESS_QUEUE_A_ROUTING_KEY, map);
        return "消息已发送至rabbitmq server";
    }
}

2.2. 消息接收方

2.2.1. 配置文件

server.port=8081

#配置rabbitmq服务器
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

spring.rabbitmq.listener.type=simple
#消费方消息确认:手动确认
spring.rabbitmq.listener.simple.acknowledge-mode=manual
spring.rabbitmq.listener.simple.default-requeue-rejected=false

2.2.2. 业务消息消费

业务队列手动接收确认消息

@Slf4j
@Component
public class DeadLetterAckReceiver {

    // 业务队列手动确认消息
    @RabbitListener(queues = "dead.letter.business.queuea")
    @RabbitHandler
    public void deadLetterReceiver1(@NotNull Message message, Channel channel) {
        try {
            // 直接拒绝消费该消息,后面的参数一定要是false,否则会重新进入业务队列,不会进入死信队列
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
            log.info("拒绝签收...消息的路由键为:" + message.getMessageProperties().getReceivedRoutingKey());
        } catch (Exception e) {
            log.info("消息拒绝签收失败", e);
        }
    }

    // 业务队列手动确认消息
    @RabbitListener(queues = "dead.letter.business.queueb")
    @RabbitHandler
    public void deadLetterReceiver2(@NotNull Message message, Channel channel) {
        try {
            // 直接拒绝消费该消息,后面的参数一定要是false,否则会重新进入业务队列,不会进入死信队列
            channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
            log.info("拒绝签收...消息的路由键为:" + message.getMessageProperties().getReceivedRoutingKey());
        } catch (Exception e) {
            log.info("消息拒绝签收失败", e);
        }
    }
}    

2.2.3. 死信消息消费

死信队列中消息的消费

@Slf4j
@Component
public class DeadLetterReceiver {

    @RabbitListener(queues = {"dead.letter.deadletter.queuea"})
    @RabbitHandler
    public void deadLetterConsumer1(@NotNull Message message, @NotNull Channel channel) {
        String msg = message.toString();
        String[] msgArray = msg.split("'");
        Map<String, String> msgMap = MapStringToMapUtil.getStringMap(msgArray[1].trim());
        String messageId = msgMap.get("messageId");
        String messageData = msgMap.get("messageData");
        String createTime = msgMap.get("createTime");
        log.info("死信队列接收到的消息为:" + "MyAckReceiver  messageId:" + messageId + "  messageData:" + messageData + "  createTime:" + createTime);
        log.info("消费的主题消息来自:" + message.getMessageProperties().getConsumerQueue());
        try {
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
        } catch (IOException e) {
            log.info("死信队列中消息的消费失败", e);
            e.printStackTrace();
        }
    }

    @RabbitListener(queues = {"dead.letter.deadletter.queueb"})
    @RabbitHandler
    public void deadLetterConsumer2(@NotNull Message message, @NotNull Channel channel) {
        String msg = message.toString();
        String[] msgArray = msg.split("'");
        Map<String, String> msgMap = MapStringToMapUtil.getStringMap(msgArray[1].trim());
        String messageId = msgMap.get("messageId");
        String messageData = msgMap.get("messageData");
        String createTime = msgMap.get("createTime");
        log.info("死信队列接收到的消息为:" + "MyAckReceiver  messageId:" + messageId + "  messageData:" + messageData + "  createTime:" + createTime);
        log.info("消费的主题消息来自:" + message.getMessageProperties().getConsumerQueue());
        try {
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
        } catch (IOException e) {
            log.info("死信队列中消息的消费失败", e);
            e.printStackTrace();
        }
    }
}

2.3. 测试

定义好之后启动程序,SpringBoot 会读取容器中类型为 QueueExchangebean 进行队列和交换机的初始化与绑定。当然也可以自己在 RabbitMQ 的管理后台进行手动创建与绑定

四个队列如下

SpringBoot整合Rabbitmq之死信队列_第2张图片
两个交换机如下

SpringBoot整合Rabbitmq之死信队列_第3张图片

测试(1

测试:调用 basicNack()basicReject(),并且参数 requeue = false,来看看死信队列。分别启动两个项目,及 rabbitmq 的服务。测试接口 http://localhost:8080/sendDeadLetterMessage1

发送端日志情况

SpringBoot整合Rabbitmq之死信队列_第4张图片
接收端日志情况

在这里插入图片描述
RabbitMQ 的管理后台页面

SpringBoot整合Rabbitmq之死信队列_第5张图片

测试(2

测试:队列设置了 x-max-length 最大消息数量且当前队列中的消息已经达到了这个数量,再次投递,消息将被挤掉,被挤掉的会进入死信队列

发送方代码变动之处

SpringBoot整合Rabbitmq之死信队列_第6张图片
队列设置 x-max-length 最大消息数量

SpringBoot整合Rabbitmq之死信队列_第7张图片

只需要启动发送方项目即可,及 rabbitmq 的服务。测试接口 http://localhost:8080/sendDeadLetterMessage1 我们连续发送 4 次请求

SpringBoot整合Rabbitmq之死信队列_第8张图片

  • 由于设置了 x-max-length = 2,队列最大长度容量只有 2
  • 发送了 4 次请求,会将先进入业务队列的 2 个请求丢弃进死信队列,后面来的 2 个请求进入业务队列

测试(3

测试:消息过期,过了 TTL 存活时间会进入死信队列

发送方设置消息过期时间
SpringBoot整合Rabbitmq之死信队列_第9张图片
只需要启动发送方项目即可,及 rabbitmq 的服务。测试接口 http://localhost:8080/sendDeadLetterMessage1

SpringBoot整合Rabbitmq之死信队列_第10张图片

5s 等待过期后

SpringBoot整合Rabbitmq之死信队列_第11张图片

3. 死信队列的应用场景

一般用在较为重要的业务队列中,确保未被正确消费的消息不被丢弃,一般发生消费异常可能原因主要有由于消息信息本身存在错误导致处理异常,处理过程中参数校验异常,或者因网络波动导致的查询异常等等,当发生异常时,当然不能每次通过日志来获取原消息,然后让运维帮忙重新投递消息。通过配置死信队列,可以让未正确处理的消息暂存到另一个队列中,待后续排查清楚问题后,编写相应的处理代码来处理死信消息,这样比手工恢复数据要好太多了

4. 小结

死信队列其实并没有什么神秘的地方,不过是绑定在死信交换机上的普通队列,而死信交换机也只是一个普通的交换机,不过是用来专门处理死信的交换机。总结一下死信消息的生命周期:

  • 业务消息被投入业务队列
  • 消费者消费业务队列的消息,由于处理过程中发生异常,于是进行了basicNack()basicReject()操作
  • basicNack()basicReject() 的消息由 RabbitMQ 投递到死信交换机中
  • 死信交换机将消息投入相应的死信队列
  • 死信队列的消费者消费死信消息

死信消息是 RabbitMQ 为我们做的一层保证,其实我们也可以不使用死信队列,而是在消息消费异常时,将消息主动投递到另一个交换机中,当你明白了这些之后,这些 ExchangeQueue 想怎样配合就能怎么配合。比如从死信队列拉取消息,然后发送邮件、短信、钉钉通知来通知开发人员关注。或者将消息重新投递到一个队列然后设置过期时间,来进行延时消费

参考:SpringBoot中RabbitMQ死信队列介绍和使用
参考:rabbitmq channel 参数详解
源码:https://gitee.com/chaojiangcj/rabbitma-deadletter

你可能感兴趣的:(中间件,#,MQ,rabbitmq)