RabbitMQ 本身并不直接提供对延迟队列的支持,我们依靠 RabbitMQ 的TTL以及死信队列功能,来实现延迟队列的效果。
死信队列
死信队列实际上是一种 RabbitMQ 的消息处理机制,当 RabbmitMQ 在生产和消费消息的时候,消息遇到如下的情况,就会变成“死信”:
- 消息被拒绝basic.reject/ basic.nack 并且不再重新投递 requeue=false
- 消息超时未消费,也就是 TTL 过期了
- 消息队列到达最大长度
消息一旦变成一条死信,便会被重新投递到死信交换机(Dead-Letter-Exchange),然后死信交换机根据绑定规则转发到对应的死信队列上,监听该队列就可以让消息被重新消费。
消息生存时间 TTL
TTL(Time-To-Live)是 RabbitMQ 的一种高级特性,表示了一条消息的最大生存时间,单位为毫秒。如果一条消息在 TTL 设置的时间内没有被消费,那么它就会变成一条死信,进入我们上面所说的死信队列。
代码架构:
配置类
@Configuration
public class TtlQueueConfig {
//普通交换机名称
public static final String NORMAL_EXCHANGE = "X";
//死信交换机名称
public static final String DEAD_EXCHANGE = "Y";
//普通队列名称
public static final String NORMAL_QUEUE_A = "QA";
public static final String NORMAL_QUEUE_B = "QB";
public static final String NORMAL_QUEUE_C = "QC";
//死信队列名称
public static final String DEAD_QUEUE = "QD";
//声明XExchange
@Bean("xExchange")
public DirectExchange xExchange() {
return new DirectExchange(NORMAL_EXCHANGE);
}
//声明YExchange
@Bean("yExchange")
public DirectExchange yExchange() {
return new DirectExchange(DEAD_EXCHANGE);
}
//声明队列QA TTL:10s
@Bean("queueA")
public Queue queueA() {
return QueueBuilder.durable(NORMAL_QUEUE_A)
.ttl(10000)
.deadLetterExchange(DEAD_EXCHANGE)
.deadLetterRoutingKey("YD")
.build();
}
//声明队列QB TTL:40s
@Bean("queueB")
public Queue queueB() {
return QueueBuilder.durable(NORMAL_QUEUE_B)
.ttl(40000)
.deadLetterExchange(DEAD_EXCHANGE)
.deadLetterRoutingKey("YD")
.build();
}
//声明队列QC 无TTL
@Bean("queueC")
public Queue queueC() {
return QueueBuilder.durable(NORMAL_QUEUE_C)
.deadLetterExchange(DEAD_EXCHANGE)
.deadLetterRoutingKey("YD")
.build();
}
//死信队列
@Bean("queueD")
public Queue queueD() {
return QueueBuilder.durable(DEAD_QUEUE).build();
}
//绑定
@Bean
public Binding queueABindingX() {
return BindingBuilder.bind(queueA()).to(xExchange()).with("XA");
}
//绑定
@Bean
public Binding queueBBindingX() {
return BindingBuilder.bind(queueB()).to(xExchange()).with("XB");
}
// 绑定
@Bean
public Binding queueCBindingX(@Qualifier("queueC") Queue queueC,
@Qualifier("xExchange") DirectExchange xExchange) {
return BindingBuilder.bind(queueC).to(xExchange).with("XC");
}
// 绑定
@Bean
public Binding queueDBindingY() {
return BindingBuilder.bind(queueD()).to(yExchange()).with("YD");
}
}
指定队列TTL:
生产者代码:
@Slf4j
@RestController
@RequestMapping("ttl")
public class SendMsgController {
@Autowired
private RabbitTemplate rabbitTemplate;
//开始发消息
@GetMapping("sendMsg/{message}")
public void sendMsg(@PathVariable("message") String message) {
log.info("当前时间:{},发送一条信息给两个TTL队列:{}", new Date().toString(), message);
rabbitTemplate.convertAndSend("X", "XA", "消息来自ttl为10s的队列:" + message);
rabbitTemplate.convertAndSend("X", "XB", "消息来自ttl为40s的队列:" + message);
}
}
消费者代码:
@Slf4j
@Component
public class DelayQueueConsumer {
//接收消息
@RabbitListener(queues = DelayedQueueConfig.DELAYED_QUEUE_NAME)
public void receiveDelayedQuere(Message message) throws Exception {
String msg = new String(message.getBody());
log.info("当前时间:{},收到延迟队列的消息:{}", new Date().toString(), msg);
}
}
结果:
注意:
由于队列的先进先出特性,只有当过期的消息到了队列的顶端(队首),才会被真正的丢弃或者进入死信队列。所以在考虑使用RabbitMQ来实现延迟任务队列的时候,需要确保业务上每个任务的延迟时间是一致的。如果遇到不同的任务类型需要不同的延时的话,需要为每一种不同延迟时间的消息建立单独的消息队列。
我们可以通过插件来优化这个问题。