MQ 延迟队列技术
MQ 延迟队列技术
1. 技术方案
在电商的支付业务中,对于一些库存有限的商品,为了更好的用户体验,通常都会在用户下单时立刻扣减商品库存。
但是这样就存在一个问题,假如用户下单后一直不付款,就会一直占有库存资源,导致其他客户无法正常交易,最终导致商户利益受损!
因此,电商中通常的做法就是:对于超过一定时间未支付的订单会自动取消订单并释放占用的库存。
但问题来了:如何才能准确的实现在下单后指定时间去检查支付状态呢?
像这种在一段时间以后才执行的任务,我们称之为延迟任务,而要实现延迟任务,最简单的方案就是利用 MQ 的延迟消息了。
在 RabbitMQ 中实现延迟消息也有两种方案:
2. 死信交换机 + TTTL
该方案利用死信交换机实现,死信交换机(Dead Letter Exchange, DLX)是一种处理消息队列中无法被消费的消息的方式。当消息因为某些原因(如消费者拒绝消费消息、消息过期等)而无法被正常消费时,这些消息就会成为 “死信”。
结合 TTL(Time To Live),你可以设定消息在队列中的存活时间,当消息在队列中停留的时间超过了设定的 TTL 后,消息就会成为死信。当消息变成死信时,会被发送到死信交换机中去,通过死信交换机转发到指定的队列,由应用程序去消费,进一步处理这些消息。
2.1 成为死信的几种情况
-
消费者使用 basic.reject 或 basic.nack 声明消费失败,并且消息的 requeue 参数设置为 false
-
消息设置了过期时间,或者消息存放的队列设置了过期时间,超过时间无人消费
-
要投递的队列消息满了,无法投递
这些就是死信,然后会通过路由规则经过交换机路由到一个队列,这个交换机就叫死信交换机,这个队列就是死信队列
2.2 死信交换机
如果这个包含死信的队列配置了 dead-letter-exchange 属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为死信交换机 (Dead Letter Exchange,简称 DLX)。
死信的路由过程:
-
消费者拒绝消费消息
-
队列绑定了死信交换机
-
死信队列有绑定的死信交换机
-
实现上述功能的条件
-
死信交换机的名称
-
死信交换机与死信队列绑定的 RoutingKey
-

2.3 TTL
超时未消费,消息变成死信的两种情况
-
消息所在的队列设置了超时时间
Message message = MessageBuilder.withBody(order.getId() .toString() .getBytes(StandardCharsets.UTF_8)) .setExpiration("30000") .build(); amqpTemplate.convertAndSend("trade.pay.direct", "dead.ttl", message); -
消息本身设置了超时时间
@Bean public Queue tradePayQueue() { return QueueBuilder.durable("trade.pay.queue") .deadLetterExchange("trade.delay.dead.exchange") .deadLetterRoutingKey("dead.ttl") .ttl(30000) // 超时时间半小时 .build(); }
如果两者都设置了,以短的时间为优先。
3. 延时队列
-
声明死信交换机和死信队列
注解方式
/** * 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); } -
声明普通交换机和普通队列、绑定关系
普通队列要把消息投递到死信交换机
/* * 正常队列绑定死信交换机,并设置过期时间 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"); } -
发消息
不设置过期时间
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

浙公网安备 33010602011771号