MQ 延迟队列技术

MQ 延迟队列技术

1. 技术方案

在电商的支付业务中,对于一些库存有限的商品,为了更好的用户体验,通常都会在用户下单时立刻扣减商品库存。

但是这样就存在一个问题,假如用户下单后一直不付款,就会一直占有库存资源,导致其他客户无法正常交易,最终导致商户利益受损!

因此,电商中通常的做法就是:对于超过一定时间未支付的订单会自动取消订单并释放占用的库存

但问题来了:如何才能准确的实现在下单后指定时间去检查支付状态呢?

像这种在一段时间以后才执行的任务,我们称之为延迟任务,而要实现延迟任务,最简单的方案就是利用 MQ 的延迟消息了。

在 RabbitMQ 中实现延迟消息也有两种方案:

2. 死信交换机 + TTTL

该方案利用死信交换机实现,死信交换机(Dead Letter Exchange, DLX)是一种处理消息队列中无法被消费的消息的方式。当消息因为某些原因(如消费者拒绝消费消息、消息过期等)而无法被正常消费时,这些消息就会成为 “死信”。

结合 TTL(Time To Live),你可以设定消息在队列中的存活时间,当消息在队列中停留的时间超过了设定的 TTL 后,消息就会成为死信。当消息变成死信时,会被发送到死信交换机中去,通过死信交换机转发到指定的队列,由应用程序去消费,进一步处理这些消息。

2.1 成为死信的几种情况

  1. 消费者使用 basic.reject 或 basic.nack 声明消费失败,并且消息的 requeue 参数设置为 false

  2. 消息设置了过期时间,或者消息存放的队列设置了过期时间,超过时间无人消费

  3. 要投递的队列消息满了,无法投递

这些就是死信,然后会通过路由规则经过交换机路由到一个队列,这个交换机就叫死信交换机,这个队列就是死信队列

2.2 死信交换机

如果这个包含死信的队列配置了 dead-letter-exchange 属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为死信交换机 (Dead Letter Exchange,简称 DLX)。

死信的路由过程:

  1. 消费者拒绝消费消息

  2. 队列绑定了死信交换机

  3. 死信队列有绑定的死信交换机

  4. 实现上述功能的条件

    1. 死信交换机的名称

    2. 死信交换机与死信队列绑定的 RoutingKey

2.3 TTL

超时未消费,消息变成死信的两种情况

  1. 消息所在的队列设置了超时时间

    Message message = MessageBuilder.withBody(order.getId()
    	.toString()
      .getBytes(StandardCharsets.UTF_8))
      .setExpiration("30000")
      .build();
    amqpTemplate.convertAndSend("trade.pay.direct", "dead.ttl", message);
    
  2. 消息本身设置了超时时间

    @Bean
    public Queue tradePayQueue() {
      return QueueBuilder.durable("trade.pay.queue")
        .deadLetterExchange("trade.delay.dead.exchange")
        .deadLetterRoutingKey("dead.ttl")
        .ttl(30000) // 超时时间半小时
        .build();
    }
    

如果两者都设置了,以短的时间为优先。

3. 延时队列

  1. 声明死信交换机和死信队列

    注解方式

    /**
     * RabbitListener:是 MQ 提供的注解,用于监听队列,并自动调用相应的方法
     *     QueueBinding:用于绑定队列和交换机,指定 routing key
     *         value:队列名称
     *         exchange:交换机名称
     *         key:routing key
     * handleNoPaySuccessQueue:是处理队列消息的方法,参数是 Message 对象,用于获取消息内容
     */
    @RabbitListener(
      bindings = @QueueBinding(
        value = @Queue(value = "trade.dead.order.queue", durable = "true"), exchange = @Exchange(value = "trade.delay.dead.exchange", type = ExchangeTypes.DIRECT), key = "dead.ttl"))
    public void handleNoPaySuccessQueue(Message message) {
      log.warn("死信交换机收到消息:{}", message);
    }
    
  2. 声明普通交换机和普通队列、绑定关系

    普通队列要把消息投递到死信交换机

    /*
     * 正常队列绑定死信交换机,并设置过期时间 30 分钟
     *
     * QueueBuilder.durable:生成持久化队列
     * deadLetterExchange:设置死信交换机
     * deadLetterRoutingKey:设置死信路由键
     * ttl:设置过期时间,单位毫秒
     */
    @Bean
    public Queue tradePayQueue() {
      return QueueBuilder.durable("trade.pay.queue")
        .deadLetterExchange("trade.delay.dead.exchange")
        .deadLetterRoutingKey("dead.ttl")
        .ttl(30000)
        .build();
    }
    
    // 设置一个普通交换机,用于接收延时消息
    @Bean
    public DirectExchange tradePayDirect() {
      return new DirectExchange("trade.pay.direct");
    }
    
    // 将 trade.pay.queue 队列绑定到 trade.pay.direct 交换机,Routing Key 为dead.ttl
    @Bean
    public Binding tradePayQueueBinding(Queue tradePayQueue, DirectExchange tradePayDirect) {
      return BindingBuilder.bind(tradePayQueue).to(tradePayDirect).with("dead.ttl");
    }
    
  3. 发消息

    不设置过期时间

    amqpTemplate.convertAndSend("trade.pay.direct", "dead.ttl", "Hello TTL!");
    log.warn("消息发送成功");
    

    设置过期时间

    Message message = MessageBuilder
      .withBody(order.getId()
      .toString()
      .getBytes(StandardCharsets.UTF_8))
      .setExpiration("3000")
      .build();
    amqpTemplate.convertAndSend("trade.pay.direct", "dead.ttl", message);
    log.warn("消息发送成功");
    

4. 使用延迟消息插件

官网Scheduling Messages with RabbitMQ | RabbitMQ

4.1 下载

下载对应所需要的版本即可:Delayed Messaging for RabbitMQ

4.2 安装

基于 Docker 安装,所以需要先查看 RabbitMQ 的插件目录对应的数据卷

docker volume inspect mq-plugins

上传下载好的插件到查询到的目录

执行命令,安装插件:

docker exec -it mq rabbitmq-plugins enable rabbitmq_delayed_message_exchange

posted @ 2025-03-24 22:53  大一点的小孩  阅读(62)  评论(0)    收藏  举报