RabbitMQ的死信队列实现延迟消息

906 阅读6分钟

一、死信队列

文档地址: www.rabbitmq.com/dlx.html

死信队列是RabbitMQ中非常重要的一个特性。简单理解,他是RabbitMQ对于未能正常消费的消息进行的一种补救机制。死信队列也是一个普通的队列,同样可以在队列上声明消费者,继续对消息进行消费处理。

​ 对于死信队列,在RabbitMQ中主要涉及到几个参数。

x-dead-letter-exchange:	mirror.dlExchange   对应的死信交换机
x-dead-letter-routing-key:	mirror.messageExchange1.messageQueue1  死信交换机routing-key
x-message-ttl:	3000  消息过期时间
durable:	true  持久化,这个是必须的。

在这里,x-dead-letter-exchange指定一个交换机作为死信交换机,然后x-dead-letter-routing-key指定交换机的RoutingKey。而接下来,死信交换机就可以像普通交换机一样,通过RoutingKey将消息转发到对应的死信队列中。

1-1、何时会产生死信

有以下三种情况,RabbitMQ会将一个正常消息转成死信

  • 消息被消费者确认拒绝。消费者把requeue参数设置为true(false),并且在消费后,向RabbitMQ返回拒绝。channel.basicReject或者channel.basicNack。
  • 消息达到预设的TTL时限还一直没有被消费。
  • 消息由于队列已经达到最长长度限制而被丢掉

TTL即最长存活时间 Time-To-Live 。消息在队列中保存时间超过这个TTL,即会被认为死亡。死亡的消息会被丢入死信队列,如果没有配置死信队列的话,RabbitMQ会保证死了的消息不会再次被投递,并且在未来版本中,会主动删除掉这些死掉的消息。

设置TTL有两种方式,一是通过配置策略指定,另一种是给队列单独声明TTL

策略配置方式 - Web管理平台配置 或者 使用指令配置 60000为毫秒单位

rabbitmqctl set_policy TTL ".*" '{"message-ttl":60000}' --apply-to queues

在声明队列时指定 - 同样可以在Web管理平台配置,也可以在代码中配置:

Map<String, Object> args = new HashMap<String, Object>();
args.put("x-message-ttl"60000);
channel.queueDeclare("myqueue"falsefalsefalse, args);

1-2、死信队列的配置方式

RabbitMQ中有两种方式可以声明死信队列,一种是针对某个单独队列指定对应的死信队列。另一种就是以策略的方式进行批量死信队列的配置。

针对多个队列,可以使用策略方式,配置统一的死信队列。、

rabbitmqctl set_policy DLX ".*" '{"dead-letter-exchange":"my-dlx"}' --apply-to queues

针对队列单独指定死信队列的方式主要是之前提到的三个属性。

channel.exchangeDeclare("some.exchange.name""direct");  
  
Map<String, Object> args = new HashMap<String, Object>();  
args.put("x-dead-letter-exchange""some.exchange.name");  
channel.queueDeclare("myqueue"falsefalsefalse, args);

这些参数,也可以在RabbitMQ的管理页面进行配置。例如配置策略时:

image.png

另外,在对队列进行配置时,只有Classic经典队列和Quorum仲裁队列才能配置死信队列,而目前Stream流式队列,并不支持配置死信队列。

1-3、关于参数x-dead-letter-routing-key

死信在转移到死信队列时,他的Routing key 也会保存下来。但是如果配置了x-dead-letter-routing-key这个参数的话,routingkey就会被替换为配置的这个值。

​ 另外,死信在转移到死信队列的过程中,是没有经过消息发送者确认的,所以并不能保证消息的安全性

1-4、如何确定一个消息是不是死信

消息被作为死信转移到死信队列后,会在Header当中增加一些消息。在官网的详细介绍中,可以看到很多内容,比如时间、原因(rejected,expired,maxlen)、队列等。然后header中还会加上第一次成为死信的三个属性,并且这三个属性在以后的传递过程中都不会更改。

  • x-first-death-reason
  • x-first-death-queue
  • x-first-death-exchange

1-5、死信队列如何消费

其实从前面的配置过程能够看到,所谓死信交换机或者死信队列,不过是在交换机或者队列之间建立一种死信对应关系,而死信队列可以像正常队列一样被消费。他与普通队列一样具有FIFO的特性。对死信队列的消费逻辑通常是对这些失效消息进行一些业务上的补偿。

RabbitMQ中,是不存在延迟队列的功能的,而通常如果要用到延迟队列,就会采用TTL+死信队列的方式来处理。

RabbitMQ提供了一个rabbitmq_delayed_message_exchange插件,可以实现延迟队列的功能,但是并没有集成到官方的发布包当中,需要单独去下载。

二、使用私信队列实现延迟消息

2-1、通过springboot的rabbitTemplate实现

在电商系统中,如果要实现订单超时15分钟未支付就将该订单交易状态改为已取消,就可以使用RabbitMQ的死信队列来实现

首先设定两个队列一个是订单在有效期内的队列orderQueue,另外一个就是死信队列deadQueue。上面提到消息变为死信的几种方式,我这边使用TTL过期时间来进行处理,设置orderQueue消息的有效期为15分钟,当消息过期的时候就会转入到deadQueue中。

2-1-1、创建死信队列存放过期的订单

死信队列和普通队列创建的方式一样,如下

@Configuration
public class DeadConfig {

    @Bean
    //创建死信交换机
    public DirectExchange deadExchange(){
        return new DirectExchange("dead.direct.exchange");
    }
    //创建死信队列
    @Bean
    public Queue deadQueue(){
        return new Queue("dead.queue");
    }
    //绑定
    @Bean
    public Binding bindQueue(){
        return BindingBuilder.bind(deadQueue()).to(deadExchange()).with("dead");
    }
}

2-1-2、创建订单交换机和队列

通过TTL+死信队列实现对过期订单的处理,核心就是如下内容,创建队列的时候需要设置如下三个参数:

(必须)x-message-ttl:当前队列消息多久未消费进入死信队列

(必须)x-dead-letter-exchange:消息过期后进入的死信队列交换机

(非必须)x-dead-letter-routing-key:消息的routingKey

@Configuration
public class OrderConfig {

    @Bean
    public DirectExchange directExchange(){
        return new DirectExchange("order.direct.exchange",true,false);
    }

    @Bean
    public Queue orderQueue(){
        HashMap<String, Object> args = new HashMap<>();
        //10秒
        args.put("x-message-ttl",10000);
        args.put("x-dead-letter-exchange","dead.direct.exchange");
        args.put("x-dead-letter-routing-key","dead");
        return new Queue("order.queue",true,false,false,args);
    }

    @Bean
    public Binding bindingQueue(){
        return BindingBuilder.bind(orderQueue()).to(directExchange()).with("dead");
    }
}

2-1-3、订单消息

@Component
public class OrderProducer {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    //  模拟订单
    public void makeTest(String a,String b){
        String ExchangeName = "order.direct.exchange";
        String luYouKey = "dead";
        String message = UUID.randomUUID().toString();
        System.out.println("订单生成----"+message);
        rabbitTemplate.convertAndSend(ExchangeName,luYouKey,message);
    }
}

2-1-4、测试

@SpringBootTest
class SpringBootDemoApplicationTests {

    @Autowired
    private OrderProducer orderProducer;


    @Test
    void contextLoads() {
        orderProducer.makeTest("下单","...");
    }

}

2-1-5、查看结果

调用完订单的消息之后,发送的消息就会进入到order.direct.exchange交换机,然后又发布到和order.direct.exchange绑定的order.queue队列中,10s后消息未消费,就会进入到dead.direct.exchange,然后再发布到和dead.direct.exchange绑定的dead.queue队列中

image.png

后面的业务如取消或者删除订单只有消费dead.queue队列的消息即可