RabbitMQ 延迟队列
什么是延迟队列
延迟队列存储的对象肯定是对应的延时消息,所谓"延时消息"是指当消息被发送以后,并不想让消费者立即拿到消息,而是等待指定时间后,消费者才拿到这个消息进行消费。
本人使用场景:
项目对接银行支付, 由于银行回调会由于某种原因如网络波动等情况导致没有接收到回调, 需要自己去查询该订单是否已经支付完成. 如在三分钟后查询支付详情.
Time To Live(TTL)
消息的TTL就是消息的存活时间
RabbitMQ 可以针对 Queue 设置过期时间或者针对 message 设置过期时间, 如果超时(两者都设置以最先到期时间为准),消息则变为死信
- 通过队列属性设置, 队列中所有消息都有相同的过期时间(推荐)
- 通过对消息单独进行设置, 每条消息的 TTL 可以不同(不推荐)
Dead Letter Exchanges(DLX)
- 一个消息在满足如下条件下,会进入死信路由,一个路由可以对应很多队列
- 一个消息被消费者拒收了, 并且reject方法的参数里requeue是false, 也就是说不会被在此放在队列里,被其他消费者使用
- 上面的消息的TTL到了,消息过期
- 队列的长度限制满了, 排在前面的消息会被丢弃或者扔到死信路由上
- 其实就是一个普通的路由, 和创建其他的Exchanges一样, 只是在某一个设置Dead Letter Exchanges的队列中有消息过期了,就会自动触发消息的转发,发送到Dead Letter Exchanges中去
- 我们既可以控制消息在一段时间后变成死信, 又可以控制变成死信的消息被路由到某个指定的交换机,结合二者,就可以实现一个延时队列
延迟队列实现方式一(推荐)
针对 Queue 设置过期时间
原理
- 生产者发送消息,使用的路由键是与死信队列绑定的路由键 bcm.qrcode.issued
- 交换机根据路由键将消息路由到死信队列
- 由于死信队列无人监听,所以在到达过期时间后继续转发到交换机, 转发的路由键是 bcm.pay.select
- 交换机在根据路由键转发的另一个监听队列中, 此时消息被消费.
- 如此实现了延迟队列, 消息在发送一段时间后才被消费者消费
代码实现
创建一个枚举类
@Getter
public enum QueueEnum {
/**
* 死信队列
*/
BCM_SELECT_QUEUE_TTL("delay_exchange", "delay_queue_per_queue_ttl", "bcm.qrcode.issued"),
/**
* 延迟队列
*/
BCM_SELECT_QUEUE("delay_exchange", "delay_process_queue", "bcm.pay.select");
/**
* 交换名称
*/
private String exchange;
/**
* 队列名称
*/
private String name;
/**
* 路由键
*/
private String routeKey;
QueueEnum(String exchange, String name, String routeKey) {
this.exchange = exchange;
this.name = name;
this.routeKey = routeKey;
}
}
配置类
@Configuration
public class MessageRabbitMqConfiguration {
/**
* 死信队列 必须无人监听
* @return
*/
@Bean
public Queue delayQueuePerQueueTTL() {
Map<String,Object> arguments = new HashMap<>();
//交换机
arguments.put("x-dead-letter-exchange",QueueEnum.BCM_SELECT_QUEUE_TTL.getExchange());
//死信后转发到监听队列的路由键
arguments.put("x-dead-letter-routing-key",QueueEnum.BCM_SELECT_QUEUE.getRouteKey());
//过期时间
arguments.put("x-message-ttl",3 * 60 * 1000);
return new Queue(QueueEnum.BCM_SELECT_QUEUE_TTL.getName(), true, false, false,arguments);
}
/**
* 延迟队列
* @return
*/
@Bean
public Queue orderReleaseOrderQueue() {
return new Queue(QueueEnum.BCM_SELECT_QUEUE.getName(), true, false, false);
}
/**
* 交换机
* @return
*/
@Bean
public Exchange orderEventExchange() {
//String name, boolean durable, boolean autoDelete, Map<String, Object> arguments
return new TopicExchange(QueueEnum.BCM_SELECT_QUEUE.getExchange(),true,false);
}
/**
* 与死信队列路由绑定
* @return
*/
@Bean
public Binding orderCreateOrderBinding() {
return new Binding(QueueEnum.BCM_SELECT_QUEUE_TTL.getName(),
Binding.DestinationType.QUEUE,
QueueEnum.BCM_SELECT_QUEUE_TTL.getExchange(),
QueueEnum.BCM_SELECT_QUEUE_TTL.getRouteKey(),
null);
}
/**
* 与延迟队列绑定
* @return
*/
@Bean
public Binding orderReleaseOrderBinding() {
return new Binding(QueueEnum.BCM_SELECT_QUEUE.getName(),
Binding.DestinationType.QUEUE,
QueueEnum.BCM_SELECT_QUEUE.getExchange(),
QueueEnum.BCM_SELECT_QUEUE.getRouteKey(),
null);
}
}
发送消息
// 发送延迟队列 3 分钟 查询支付详情
BCMMessage message = new BCMMessage();
message.setMerTranNo(orderNo);
message.setTranType("PAY");
message.setCount(1);
//主要此处使用的是死信队列的路由键
rabbitMqTemplate.sendMessage(message, QueueEnum.BCM_SELECT_QUEUE_TTL.getRouteKey(), QueueEnum.BCM_SELECT_QUEUE_TTL.getExchange());
监听消息
@Component
@Slf4j
@AllArgsConstructor
@RabbitListener(queues = "delay_process_queue")
public class BcmMsgConsumer {
@RabbitHandler
public void bcmPaySelect(@Payload BCMMessage message) {
log.info("BcmMsgConsumer-bcmPaySelect-message: {}", message.toString());
// 业务处理代码
...
//如果还是失败继续尝试,共三次
if (message.getCount() < 3){
message.setCount(message.getCount() + 1);
rabbitMqTemplate.sendMessage(message, QueueEnum.BCM_SELECT_QUEUE_TTL.getRouteKey(), QueueEnum.BCM_SELECT_QUEUE_TTL.getExchange());
}
}
}
延迟队列实现方式二(不推荐)
针对 message 设置过期时间 expiration : 180000
原理
- 生产者生产消息,并对消息设置单独的过期时间 expiration : 180000
- 在无人监听的死信队列中当消息到达过期时间后转发到交换机
- 交换机在根据路由键转发到监听队列, 此时消息被消费.
- 如此实现了延迟队列, 消息在发送一段时间后才被消费者消费
缺点
由于RabbitMQ采用的是惰性检查机制,也就是懒检查,如果第一个是5分钟过期, 第二个是3分钟过期,服务器拿出第一个消息发现是5分钟后过期,则5分钟后再来拿取,所以第二个设置的3分钟过期消息得在第一个消息过期之后才能过期