RabbitMQ延迟队列(TTL、DXL)

2,662 阅读8分钟

🚦延迟队列

在一般的队列中,消息一旦入了队列,消息就可以被消费者消费掉,而延迟队列就是队列里的消息不会被立即消费,而是经过一定的时间后队列中的消息才可以被拿来消息,延迟消息的消费。实现延迟队列的方式有很多种,比如JDK提供的原生的DelayQueue延迟队列、通过Redis的zset数据类型来实现的延迟队列、RabbitMQ延迟队列。下面我们主要介绍RabbitMQ中的延迟队列是如何实现的。

RabbitMQ延迟队列

首先我们需要明确,RabbitMQ并没有直接为我们提供延迟队列的功能,而是通过DLX(Dead Letter Exchange)死信交换机和TTL(Time To Live)过期时间来实现延迟队列功能的。

TTL过期时间

TTLTime To Live)表示可以对消息设置预期的时间,在这个时间内都可以被消费者接收获取,过了时间之后消息将自动被删除。RabbitMQ可以对消息和队列设置TTL,目前有两种方法可以设置:

  • 针对队列设置x-expires过期时间,队列中所有的消息都有相同的过期时间,超过该设定的时间,队列会被自动删除
  • 针对消息设置 x-message-ttl,每条消息在队列中只会存活设定的时间

✨如果上述两种方法一起使用,则消息的过期时间以两者之间的最小值为准。消息在队列的生存时间一旦超过设置的TTL值,就称为死信(Dead Message)被投递到死信队列。如果设置了TTL过期时间的队列通过死信交换机与一个死信队列进行绑定,那么当消息在队列的生存时间超过了TTL值,队列中的消息就会被转移到死信队列中,而不会被丢弃。

DXL死信交换机

DLX(Dead-Letter-Exchange) ,可以称之为死信交换机或者死信邮箱。当消息在一个消息队列中变成死信(dead message)之后,它能被重新发送到另外一个交换机中,这个交换机就是DLX,绑定到DLX交换机的队列就称之为死信队列。DLX也是一个普通的交换机,他能在任何的队列上被绑定,实际上就是设置某一个队列的属性,当这个队列中存在死信(即过期的消息)时,RabbitMq就会自动地将这个消息重新发布到设置的DLX上去,进而被路由到另外队列,即死信队列,要想使用死信队列,只需要在定义队列的时候设置队列参数 x-dead-letter-exchange 指定对应的死信交换机即可。 消息变成死信,可能由于以下的原因:

  • 消息被消费端拒绝(basicReject 或者 basicNack并且requeue=false消息不重回队列)
  • 消息过期
  • 队列达到最大长度

定义队列时的几个重要参数Arguments

  • x-dead-letter-exchange:当消息过期时投递到的死信交换机

  • x-dead-letter-routing-key :死信交换机根据当前重新指定的routin key将消息重新路由到死信队列(如果死信交换机为fanout类型则无需指定)。当出现死信时就会通过该参数指定的routing key和上面参数指定的死信交换机将消息重新路由转发到死信队列。

  • x-message-ttl:队列中消息的过期时间,单位为毫秒

  • x-max-length:表示队列中能存储的最大消息数,超过该数值后,消息变为死信

  • x-expires:表示超过该设定的时间,队列将被自动删除

RabbitMQ延迟队列实现原理

  • 整体结构

image.png

🎃从以上可以看出,消息生产者发送消息到交换机后路由到一个指定消息过期时间的队列中,而这个队列没有消费者监听,因而在过期时间内消息不会被消费掉,当消息过期时间到后,该消息就变成了一个死信,会被重新发送到DLX死信交换机上,消息到达DLX死信交换机后就会被路由到与之绑定的队列,即被路由到死信队列,消费者监听死信队列,获取生产者生产的消息并消费。经过以上流程,我们就可以实现一个简单的延迟队列。如当设置队列中消息的过期时间x-message-ttl=10000(即10秒之后消息就会过期),那么在发送10秒之内我们消费者无法获取到消息,10秒之后我们才能取得消息来消费,从而实现消息的延迟消费。

🎁延迟队列在我们的业务开发中经常会被使用到,例如当一个订单超过某个时间点后还没有被支付,该订单就会被自动取消,如果有绑定死信队列则可以将过期的消息重新发送到死信交换机上,由交换机将过期消息投递到死信队列中,作为历史订单消息。

上手小案例

web端体验延迟队列的实现

  • 首先我们根据上面的整体结构图来定义声明相应的队列和交换机
  1. 定义一个死信交换机,类型为Direct,名称为direct-dead-exchange

image.png

  1. 定义一个死信队列,用来接收重新投递的死信,名称为direct-dead-queue,并将其绑定到死信队列上,指定binding key为dead

image.png

  1. 定义一个Direct类型的交换机,名称为direct-ttl-exchange

image.png

  1. 定义一个指定消息过期时间的队列,名称为direct-ttl-queue,其中设置参数x-dead-letter-exchange = direct-dead-exchange表示当该队列中的消息过期变成死信后重新发送到死信交换机direct-dead-exchange中、x-dead-letter-routing-key = dead表示当死信到达死信交换机后根据该路由键重新投递到死信队列中、x-message-ttl = 10000表示队列中消息的过期时间。定义好后,将队列绑定到交换机direct-ttlxchange上面,banding key 为ttl,如下:

image.png

  1. 完成上面的定义和配置后,我们往交换机direct-ttl-exchange中发送一条消息,看看会出现什么情况。
  • 发送

image.png

  • 首先,我们通过web端可以看到队列direct-ttl-queue首先接收到消息,并且只存活了10秒,就被重新投递到死信队列,如下

image.png

  • 死信队列接收到重新投递的消息

image.png

🎏以上就是通过Web端的方式来实现一个延迟队列,下面我们通过代码的形式来实现一个延迟队列。

SpringBoot+RabbitMQ实现延迟队列

步骤同在Web端使用一样,只是我们以编码的方式来实现

  1. 定义一个死信交换机,类型为Direct,名称为direct-dead-exchange
@Bean
public DirectExchange deadDirectExchange(){
    return new DirectExchange("direct-dead-exchange",true,false);
}
  1. 定义一个死信队列,用来接收重新投递的死信,名称为direct-dead-queue,并将其绑定到死信队列上,指定binding key为dead
@Bean
public Queue deadQueue(){
    return new Queue("direct-dead-queue",true,false,false);
}
//将死信队列绑定到交换机上,指定binding key为dead
@Bean
public Binding bindingDead(){
    return BindingBuilder.bind(deadQueue()).to(deadDirectExchange()).with("dead");
}

3 定义一个Direct类型的交换机,名称为direct-ttl-exchange

@Bean
public DirectExchange ttlDirectExchange(){
    return new  DirectExchange("direct-ttl-exchange",true,false);
}
  1. 定义一个指定过期时间的的队列(并配置参数),名称为direct-ttl-queue,并将其绑定到direct-ttl-exchange交换机上
@Bean
public Queue ttlQueue(){
    HashMap<String, Object> args = new HashMap<>();
    //队列中消息的过期时间
    args.put("x-message-ttl",1000);
    //当消息变为死信后重新发送到指定死信交换机
    args.put("x-dead-letter-exchange","direct-dead-exchange");
    //当死信到达死信交换机后,根据该路由键投递到指定的死信队列
    args.put("x-dead-letter-routing-key","dead");
    return new Queue("direct-ttl-queue",true,false,false,args);
}
//将指定了消息过期时间的队列绑定到交换机direct-ttl-exchange上
//指定binding key为ttl
@Bean
public Binding bindingTll(){
    return BindingBuilder.bind(ttlQueue()).to(ttlDirectExchange()).with("ttl");
}
  1. 定义一个消费者,来监听死信队列,获取在direct-ttl-queue队列上过期的消息。
@Component
public class Consumer {
    
    @RabbitListener(queues="dead-direct-queue")
    public void getMessage(Object msg, Channel channel , Message message) throws IOException {
        System.out.println("消费时间:"+ LocalDateTime.now());
        System.out.println("消费者接收到的消息:"+msg);
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
    }
}
  1. 定义一个Controller来接收请求并发送消息
@PostMapping("/putoff")
@ResponseBody
public RespBean putOffQueue(){
    String exchange="direct-ttl-exchange";
    String routingKey="ttl";
    rabbitTemplate.convertAndSend(exchange,routingKey,"延迟队列测试---不喝奶茶的Pogrammer");
    return RespBean.success("发送成功");
}
  • 经过以上的定义声明,接下来我们可以在Postman测试一下,往direct-ttl-exchange交换机上发送一条消息。

image.png

  • 从以下控台的输出结果可以看出,消息发送后,并没有直接被消费掉,而是10秒之后变成死信,然后被重新投递到死信队列,消费者监听死信队列,获取死信,从而实现了消息的延迟消费。

image.png

总结

经过上面的了解,我们知道了RabbitMQ延迟队列是基于TTL过期时间以及DXL死信交换机来实现的,其过程并不复杂,实现起来也比较简单。在下一篇文章中,将介绍RabbitMQ延迟队列是如何确保生产端可靠性投递的。

🏁以上就是对RabbitMQ延迟队列的详细解释,如果有错误的地方,还请留言指正,如果觉得本文对你有帮助那就点个赞👍吧😋😻😍

默认标题_动态分割线_2021-07-15-0.gif