SpringBoot 集成 RabbitMq 实现死信队列和延时队列

一、死信队列

1、什么是死信队列?

死信队列其实和普通队列并没有什么区别,就是一个普通的队列,当消息“死掉”成为 Dead Message 之后,就会被重新发送到一个交换机,这个交换机就是死信交换机,与死信交换机绑定的队列就是死信队列,死信交换机简称 DLX ,Dead Letter Exchange

2、什么样的消息会“死掉” 呢?

简单来说就是无法被消费的消息会成为死信,那么消息成为死信有下面几种情况

  1. 消费者使用 basic.reject 和 basic.nack 拒绝签收消息,并且配置 requeue 参数是 false(即不重回原来的队列)
    例如,当消费者端开启了消息的手动确认,消费者端在接收到消息之后,不想消费当前消息,进行消息拒绝签收操作,消息就会成为死信

  2. 消息在队列中的时间超过配置的 TTL 存活时间
    例如:队列配置消息存活时间为 5s,那么消息在 5s 内未被消费就会成为死信

  3. 队列中消息的数量超过最大长度
    例如,配置队列最大消息长度为1,当队列消息超过1时,消息成为死信

所有的普通队列都可以配置死信交换机
当前我们基于一个普通的订单队列来进行模拟,我们给订单队列配置一个死信交换机,通过模拟订单队列出现的异常情况,使消息变成死信,进入死信队列

3、配置队列和交换机

@Configuration
public class DeadLetterConfig {

    // 订单队列
    public static final String ORDER_QUEUE = "order_queue";
    // 订单交换机
    public static final String ORDER_EXCHANGE = "order_exchange";


    /**
     * 订单队列
     */
    @Bean
    public Queue orderQueue() {
        Map map = new HashMap<>(2);
        // 绑定该队列到死信交换机
        map.put("x-dead-letter-exchange", DEAD_EXCHANGE);
//        // 消息 RoutingKey 路由模式需要设置,当前使用发布订阅模式不需要配置 routingKey
//        map.put("x-dead-letter-routing-key","routingKey");
        return new Queue(ORDER_QUEUE, true, false, false, map);
    }

    /**
     * 订单交换机
     */
    @Bean
    public FanoutExchange orderExchange() {
        return new FanoutExchange(ORDER_EXCHANGE);
    }

    /**
     * 订单队列和交换机绑定
     */
    @Bean
    public Binding bindingOrderExchange() {
        return BindingBuilder.bind(orderQueue()).to(orderExchange());
    }


    // ******************************* 死信队列 ************************************

    // 死信队列
    public static final String DEAD_QUEUE = "dead_queue";
    // 死信交换机
    public static final String DEAD_EXCHANGE = "dead_exchange";


    /**
     * 死信队列
     */
    @Bean
    public Queue deadQueue() {
        return new Queue(DEAD_QUEUE);
    }

    /**
     * 死信交换机
     */
    @Bean
    public FanoutExchange deadExchange() {
        return new FanoutExchange(DEAD_EXCHANGE);
    }

    /**
     * 死信队列和死信交换机绑定
     */
    @Bean
    public Binding bindingDeadExchange() {
        return BindingBuilder.bind(deadQueue()).to(deadExchange());
    }

}

订单队列配置交换机之后,会显示 DLX 的标识
SpringBoot 集成 RabbitMq 实现死信队列和延时队列_第1张图片

4、实现死信队列

4.1、消费者拒收消息产生死信

流程:订单生产者发送一条消息,订单消费者进行消息拒收,并且设置消息不重回队列,此时消息会变成死信,监听死信队列的消费者会消费到这条消息

订单生产者发送消息

    @RequestMapping("/orderSend")
    public void orderSend() {
        String message = "order Message";
        log.info("订单队列生产者发送消息 :{}", message);
        rabbitTemplate.convertAndSend(DeadLetterConfig.ORDER_EXCHANGE, "", message);
    }

订单消费者拒收消息

    /**
     * 订单队列消费者
     *
     * @param message 消息内容
     */
    @RabbitListener(queues = DeadLetterConfig.ORDER_QUEUE)
    public void orderConsumer(Message message, Channel channel) throws IOException {
        log.info("订单队列消费者接收到消息:{}", message);
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        channel.basicNack(deliveryTag,false,false);
    }

死信队列

    /**
     * 死信队列消费者
     *
     * @param message 消息内容
     */
    @RabbitListener(queues = DeadLetterConfig.DEAD_QUEUE)
    public void deadConsumer(Message message, Channel channel) throws IOException {
        log.info("死信队列消费者接收到消息:{}", message);
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        channel.basicAck(deliveryTag,false);
    }

这里模拟,订单消费者使用了 basicNack ,使用 basicReject 也是一样的,消息会进入死信队列
SpringBoot 集成 RabbitMq 实现死信队列和延时队列_第2张图片

4.2、消息超过TTL时间变成死信

流程:给订单队列配置一个 TTL 过期时间,过期时间设置为3秒,删除原本的订单队列,注释掉订单消费者的代码,通过订单生产者发送一条消息,当消息在过期时间内未被消费,消息进入死信队列

更改 DeadLetterConfig 配置文件中 订单队列的配置

    /**
     * 订单队列
     */
    @Bean
    public Queue orderQueue() {
        Map map = new HashMap<>(2);
        // 绑定该队列到死信交换机
        map.put("x-dead-letter-exchange", DEAD_EXCHANGE);
//        // 消息 RoutingKey 路由模式需要设置,当前使用发布订阅模式不需要配置 routingKey
//        map.put("x-dead-letter-routing-key","routingKey");
        // 消息3秒后过期
        map.put("x-message-ttl",3000);
        return new Queue(ORDER_QUEUE, true, false, false, map);
    }

注意:这里更改队列配置之后,要先在 rabbitMq 的 web 管理页面先删除原来的 order_queue 队列,否则直接重启会报错,需要先删除,然后让程序重新创建队列

改变队列配置之后,不手动删除原本的订单队列就会报以下截图错误
SpringBoot 集成 RabbitMq 实现死信队列和延时队列_第3张图片

删除原本订单队列
SpringBoot 集成 RabbitMq 实现死信队列和延时队列_第4张图片

给订单队列设置了 TTL 过期时间之后,重新创建的订单队列会有一个 TTL 的标识
SpringBoot 集成 RabbitMq 实现死信队列和延时队列_第5张图片

注释掉订单消费者的代码,订单生产者发送一条消息,三秒之后未被消费,消息进入死信队列
SpringBoot 集成 RabbitMq 实现死信队列和延时队列_第6张图片

4.3、队列消息长度超过配置长度变成死信

流程:给订单队列配置一个 Lim 队列最大未消费消息长度,长度设置为2,删除原本的订单队列,注释掉订单消费者的代码,通过订单生产者发送两条消息,当队列中未被消费消息到达两条时,第二条消息进入死信队列

更改 DeadLetterConfig 配置文件中 订单队列的配置

    /**
     * 订单队列
     */
    @Bean
    public Queue orderQueue() {
        Map map = new HashMap<>(2);
        // 绑定该队列到死信交换机
        map.put("x-dead-letter-exchange", DEAD_EXCHANGE);
//        // 消息 RoutingKey 路由模式需要设置,当前使用发布订阅模式不需要配置 routingKey
//        map.put("x-dead-letter-routing-key","routingKey");
//        // 消息3秒后过期
//        map.put("x-message-ttl",3000);
        // 设置队列的最大长度值为1
        map.put("x-max-length", 1);
        return new Queue(ORDER_QUEUE, true, false, false, map);
    }

同4.2一样,需要先删除原有的订单队列,否则如下图
SpringBoot 集成 RabbitMq 实现死信队列和延时队列_第7张图片

配置队列长度之后,订单队列会有一个 Lim 的标识
SpringBoot 集成 RabbitMq 实现死信队列和延时队列_第8张图片

通过订单生产者发送两条消息,记得要注释掉订单消费者的代码,当第二条消息进入队列时,第二条变成死信,会将第二条消息发送到死信队列
SpringBoot 集成 RabbitMq 实现死信队列和延时队列_第9张图片

订单队列中剩下一条未消费的消息
SpringBoot 集成 RabbitMq 实现死信队列和延时队列_第10张图片

5、死信队列总结

  1. 每个队列都可以设置死信队列
  2. 死信队列我们可以理解为普通队列的消息兜底,普通队列出现异常时,消息发送到死信队列,方便定位问题
  3. 死信队列可以确保我们重要的消息不被丢失

二、延时队列

什么是延时队列?
延时队列就是,放在这个队列里面的消息不像普通队列一样,一有消息就要马上消费掉,需要延期一段时间再消费

为什么需要延时队列?
如果你用过唯品会,就会发现它有这样一种功能,我们将商品加入到购物车之后,你会发现,20分钟之后,这个商品就会从你的购物车删掉,也就是说,它会自动清理20分钟内购物车没有下单的商品
还有一些其他场景,例如
订单下单30分钟之后没有付款,自动取消订单
会议开始前15分钟前提醒等等
像诸如此类的场景,我们就可以通过延时队列来实现,那么我们接下来使用 RabbitMq 通过两种方式来实现延时队列

1、TTL 队列+死信队列实现延时队列

流程:创建一个订单队列,订单队列配置 TTL 过期时间为3秒,配置死信队列,不需要监听订单队列的消费者,需要监听订单队列设置的死信队列

1.1、TTL 队列

DeadLetterConfig 配置文件基本一致

 @Configuration
public class DeadLetterConfig {

    // 订单队列
    public static final String ORDER_QUEUE = "order_queue";
    // 订单交换机
    public static final String ORDER_EXCHANGE = "order_exchange";


    /**
     * 订单队列
     */
    @Bean
    public Queue orderQueue() {
        Map map = new HashMap<>(2);
        // 绑定该队列到死信交换机
        map.put("x-dead-letter-exchange", DEAD_EXCHANGE);
//        // 消息 RoutingKey 路由模式需要设置,当前使用发布订阅模式不需要配置 routingKey
//        map.put("x-dead-letter-routing-key","routingKey");
        // 消息3秒后过期
        map.put("x-message-ttl",3000);
        return new Queue(ORDER_QUEUE, true, false, false, map);
    }

    /**
     * 订单交换机
     */
    @Bean
    public FanoutExchange orderExchange() {
        return new FanoutExchange(ORDER_EXCHANGE);
    }

    /**
     * 订单队列和死信交换机绑定
     */
    @Bean
    public Binding bindingOrderExchange() {
        return BindingBuilder.bind(orderQueue()).to(orderExchange());
    }


    // ******************************* 死信队列 ************************************

    // 死信队列
    public static final String DEAD_QUEUE = "dead_queue";
    // 死信交换机
    public static final String DEAD_EXCHANGE = "dead_exchange";


    /**
     * 死信队列
     */
    @Bean
    public Queue deadQueue() {
        return new Queue(DEAD_QUEUE);
    }

    /**
     * 死信交换机
     */
    @Bean
    public FanoutExchange deadExchange() {
        return new FanoutExchange(DEAD_EXCHANGE);
    }

    /**
     * 死信队列和死信交换机绑定
     */
    @Bean
    public Binding bindingDeadExchange() {
        return BindingBuilder.bind(deadQueue()).to(deadExchange());
    }

}

订单生产者发送消息

    @RequestMapping("/orderSend")
    public void orderSend() {
        String message = "order Message";
        log.info("订单队列生产者发送消息 :{}", message);
        rabbitTemplate.convertAndSend(DeadLetterConfig.ORDER_EXCHANGE, "", message);
    }

注意:这里我们就不用再监听订单的的队列了,直接监听死信队列即可,将我们的业务代码在死信队列消费者中实现

    /**
     * 死信队列消费者
     *
     * @param message 消息内容
     */
    @RabbitListener(queues = DeadLetterConfig.DEAD_QUEUE)
    public void deadConsumer(Message message, Channel channel) throws IOException {
        log.info("死信队列消费者接收到消息:{}", message);
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        // todo 订单业务处理,判断改订单状态是否已支付,如果未支付,删除该订单,已支付则不做操作
        channel.basicAck(deliveryTag,false);
    }

订单生产者发送一条消息,3秒之后消息就进入到了死信队列中SpringBoot 集成 RabbitMq 实现死信队列和延时队列_第11张图片

原理
其实这种方式就是,根据我们自己的业务员需求,设置队列内消息的过期时间,只是我这里测试设定的3秒过期,假定订单20分钟未支付取消订单,那么我们设定队列过期时间为20分钟,指定对应的死信队列就可以,然后监听死信队列,在消费死信队列中消息做具体业务逻辑即可

那么这种方式会存在一个问题,假如我们有很多订单,那就需要设定很多队列,每个队列指定过期时间

接下来解决这个问题

1.2、TTL 消息

我们动态指定生产者每一条消息的 TTL 过期时间,这样就不用创建许多队列了,一个队列,每条消息 TTL 时间不一致

更改 DeadLetterConfig 配置文件
去掉队列的 TTL 配置

    /**
     * 订单队列
     */
    @Bean
    public Queue orderQueue() {
        Map map = new HashMap<>(2);
        // 绑定该队列到死信交换机
        map.put("x-dead-letter-exchange", DEAD_EXCHANGE);
//        // 消息 RoutingKey 路由模式需要设置,当前使用发布订阅模式不需要配置 routingKey
//        map.put("x-dead-letter-routing-key","routingKey");
        return new Queue(ORDER_QUEUE, true, false, false, map);
    }

订单生产者
生产消息时,指定消息的 TTL 时间为 3秒

    @RequestMapping("/orderSend")
    public void orderSend() {
        String message = "order Message";
        log.info("订单队列生产者发送消息 :{}", message);

//        rabbitTemplate.convertAndSend(DeadLetterConfig.ORDER_EXCHANGE, "", message);

        // 设置队列单条消息的 TTL 过期时间
        rabbitTemplate.convertAndSend(DeadLetterConfig.ORDER_EXCHANGE, "", message, message1 -> {
                    message1.getMessageProperties().setExpiration("3000");
                    return message1;
                }
        );
    }

重新创建队列之后,TTL 的标识就没有了
SpringBoot 集成 RabbitMq 实现死信队列和延时队列_第12张图片
我们重新测试,一样可以达到3秒过期的效果
SpringBoot 集成 RabbitMq 实现死信队列和延时队列_第13张图片

注意:这里我们虽然设置队列中每条消息的 TTL 过期时间比较灵活方便,但是这种方式会存在一个问题,假如,我们往队列中写入两条消息,第一条A消息过期时间为5秒,第二条B消息过期时间为3秒,按照正常逻辑,我们希望3秒的消息,先进入死信队列,然后5秒的消息再进入死信队列,但是事实可不是这样,因为消息是先进先出,所以我们会看到等到 A 消息过期之后,B消息跟着马上过期

这里测试,复制一个订单生产者接口出来,分别设置5秒,3秒过期
SpringBoot 集成 RabbitMq 实现死信队列和延时队列_第14张图片
然后我们先调用 /orderSend,再调用 /orderSend2,就会出现这种情况

SpringBoot 集成 RabbitMq 实现死信队列和延时队列_第15张图片

2、插件实现延时队列

2.1、下载 RabbitMq 延时插件

下载地址:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/tags

注意版本对应,我的 RabbitMq 版本是 3.9.13
SpringBoot 集成 RabbitMq 实现死信队列和延时队列_第16张图片

这里我就选择 3.9 的版本下载了
SpringBoot 集成 RabbitMq 实现死信队列和延时队列_第17张图片
如果版本不对应,安装时会提示
在这里插入图片描述

这里我们,点到 3.9.0 版本里面去下载,选择 .ez 后缀的文件,如果服务器能直接下载,直接复制链接在线下载,不能在线就先下载后面再上传到服务器
SpringBoot 集成 RabbitMq 实现死信队列和延时队列_第18张图片

下载命令

wget https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/download/3.9.0/rabbitmq_delayed_message_exchange-3.9.0.ez

注意要下载到你的 RabbitMq 插件目录
这是我的目录,可以参考
在这里插入图片描述
直接在线下载
SpringBoot 集成 RabbitMq 实现死信队列和延时队列_第19张图片

2.2、安装插件

执行命令

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

SpringBoot 集成 RabbitMq 实现死信队列和延时队列_第20张图片

2.3、通过延时插件实现延时队列

创建配置文件,注意这里的交换机类型

@Configuration
public class DelayConfig {

    // 延时队列
    public static final String DELAY_QUEUE = "delay_queue";
    // 延时交换机
    public static final String DELAY_EXCHANGE = "delay_exchange";
    public static final String DELAY_KEY = "delay_key";


    /**
     * 延时队列
     */
    @Bean
    public Queue delayQueue() {
        return new Queue(DELAY_QUEUE, true);
    }

    /**
     * 延时交换机
     */
    @Bean
    public CustomExchange delayExchange() {
        Map<String, Object> map = new HashMap<>();
        map.put("x-delayed-type", "direct");
        // 交换机名称 交换机类型 是否持久化 是否自动删除 配置参数
        return new CustomExchange(DELAY_EXCHANGE,"x-delayed-message",true,false,map);
    }

    /**
     * 延时队列和交换机绑定
     */
    @Bean
    public Binding bindingDelayExchange() {
        return BindingBuilder.bind(delayQueue()).to(delayExchange()).with(DELAY_KEY).noargs();
    }
}

延时队列生产者

    @RequestMapping("/delaySend")
    public void delaySend() {
        String message = "delay Message";
        log.info("延时队列生产者发送消息 :{}", message);
        // 设置队列单条消息的延期时间
        rabbitTemplate.convertAndSend(DelayConfig.DELAY_EXCHANGE, DelayConfig.DELAY_KEY, message, message1 -> {
                    message1.getMessageProperties().setDelay(5000);
                    return message1;
                }
        );
    }

延时队列消费者

    /**
     * 延时队列消费者
     *
     * @param message 消息内容
     */
    @RabbitListener(queues = DelayConfig.DELAY_QUEUE)
    public void delayConsumer(Message message, Channel channel) throws IOException {
        log.info("延时队列消费者接收到消息:{}", message);
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        // todo 订单业务处理,判断改订单状态是否已支付,如果未支付,删除该订单,已支付则不做操作
        channel.basicAck(deliveryTag,false);
    }

延时队列生产者发送消息,消费者5秒之后消费此消息
SpringBoot 集成 RabbitMq 实现死信队列和延时队列_第21张图片

注意
我们再次模拟 二、1.2 步骤中,我们最后模拟的单条消息 TTL 场景
为了测试,新增一个3秒延时消息接口
SpringBoot 集成 RabbitMq 实现死信队列和延时队列_第22张图片

然后先发送5秒延时消息,再发送3秒延时消息,这里可以看到 3秒的延时消息,优先被消费到了,然后5秒的延时消息被消费,由此可见,使用延时插件,就避免了我们通过死信队列设置单条消息 TTL 实现延时队列的缺陷。
SpringBoot 集成 RabbitMq 实现死信队列和延时队列_第23张图片

3、延时队列总结

  1. 可以通过设置队列的 TTL 和 DLX 死信队列属性来实现延时队列,也可以通过延时插件来实现
  2. 可以设置队列的 TTL 也可以设置每条消息的 TTL,但是都会存在一定的弊端,设置队列的 TTL 就会导致队列数量增多,设置消息的 TTL 则会因为队列的先进先出,导致已超过 TTL 时间的消息却还未发送到死信队列
  3. 延时插件需要配置延时队列,设置每条消息的延时消费时间,消费者延时消费

源码

注意,看代码的话,请仔细一点

https://github.com/wxwhowever/springboot-notes/tree/master/rabbitmq-demo

你可能感兴趣的:(springboot,java-rabbitmq,rabbitmq,spring,boot)