在很多的业务场景中,延时队列可以实现很多功能,此类业务中,一般上是非实时的,需要延迟处理的,需要进行重试补偿的。
本身在RabbitMQ中是未直接提供延时队列功能的,但可以使用 TTL(Time-To-Live,存活时间) 和 DLX(Dead-Letter-Exchange,死信队列交换机) 的特性实现延时队列的功能。
RabbitMQ中可以对队列和消息分别设置TTL,TTL表明了一条消息可在队列中存活的最大时间。当某条消息被设置了TTL或者当某条消息进入了设置了TTL的队列时,这条消息会在TTL时间后死亡成为Dead Letter。如果既配置了消息的TTL,又配置了队列的TTL,那么较小的那个值会被取用。
设置了TTL的消息或队列最终会成为Dead Letter,当消息在一个队列中变成死信之后,它能被重新发送到另一个交换机中,这个交换机就是DLX,绑定此DLX的队列就是死信队列。
一个消息变成死信一般上是由于以下几种情况;
所以,通过TTL和DLX的特性可以模拟实现延时队列的功能。当队列中的消息超时成为死信后,会把消息死信重新发送到配置好的交换机中,然后分发到真实的消费队列。故简单来说,我们可以创建2个队列,一个队列用于发送消息,一个队列用于消息过期后的转发的目标队列。
下面是死信队列在创建、绑定、生产消息、消费消息过程的结构流程图:
图中问题的答案为:当入死信队列的消息TTL一到,它自然而然的将被路由到 死信交换机绑定的队列 中被真正消费处理!!!
项目使用上一篇中的项目 rabbitmq-produce、rabbitmq-consumer
DealRabbitConfig 的代码如下:
package com.example.rabbitmqproduce.config;
import com.google.common.collect.Maps;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
/**
* 死信队列
*/
@Configuration
public class DealRabbitConfig {
private Logger logger = LoggerFactory.getLogger(DealRabbitConfig.class);
//死信队列 模型
@Bean
public Queue dealQueue() {
Map argsMap = Maps.newHashMap();
//当变成死信队列时,会转发至 路由为x-dead-letter-exchange及x-dead-letter-routing-key的队列中
argsMap.put("x-dead-letter-exchange", "deal.exchange");
argsMap.put("x-dead-letter-routing-key", "deal.routing.key");
//过期时间(单位:毫秒),当过期后 会变成死信队列,之后进行转发
//TTL 这边可以不填,可以动态设置,通过MessageProperies属性设定
argsMap.put("x-message-ttl", 10000);
return new Queue("deal.queue", true, false, false, argsMap);
}
//生产端的交换机
@Bean
public TopicExchange produceExchange(){
return new TopicExchange("produce.exchange",true,false);
}
//生产端的交换机和路由key 绑定 死信队列
@Bean
public Binding produceToDeal(){
return BindingBuilder.bind(dealQueue()).to(produceExchange()).with("produce.routing.key");
}
//消费端的队列
@Bean
public Queue consumerQueue(){
return new Queue("consumer.queue",true);
}
//死信交换机
@Bean
public TopicExchange deadExchange(){
return new TopicExchange("deal.exchange",true,false);
}
//死信交换机+死信路由key->消费端的队列
@Bean
public Binding dealToConsumer(){
return BindingBuilder.bind(consumerQueue()).to(deadExchange()).with("deal.routing.key");
}
}
RabbitDealProduce 的代码如下:
package com.example.rabbitmqproduce.produce;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* 死信队列 消息生产者
* @Component 注入到Spring容器中
*/
@Component
public class RabbitDealProduce {
//注入一个AmqpTemplate来发布消息
@Autowired
private AmqpTemplate rabbitTemplate;
private Logger logger = LoggerFactory.getLogger(RabbitDealProduce.class);
/**
* topicA 发送消息
*/
public void send() {
String messageId = String.valueOf(UUID.randomUUID());
String messageData = "hello!死信队列";
String createTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
Map map=new HashMap<>();
map.put("messageId",messageId);
map.put("messageData",messageData);
map.put("createTime",createTime);
logger.info("发送的内容 : " + map.toString());
rabbitTemplate.convertAndSend("produce.exchange", "produce.routing.key", map);
}
}
代码如下:
package com.example.rabbitmqproduce.controller;
import com.example.rabbitmqproduce.produce.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("TestController")
public class TestController {
private static final Logger logger = LoggerFactory.getLogger(TestController.class);
@Autowired
private RabbitMqProduce rabbitMqProduce;
@Autowired
private DirectExchangeProduce directExchangeProduce;
@Autowired
private FanoutExchangeProduce fanoutExchangeProduce;
@Autowired
private TopicExchangeProduce topicExchangeProduce;
@Autowired
private RabbitAckProduce rabbitAckProduce;
@Autowired
private RabbitDealProduce rabbitDealProduce;
/**
* 测试基本消息模型(简单队列)
*/
@RequestMapping(value = "/testSimpleQueue", method = RequestMethod.POST)
public void testSimpleQueue() {
logger.info("测试基本消息模型(简单队列)SimpleQueue---开始");
for (int i = 0; i < 10; i++) {
rabbitMqProduce.sendMessage();
}
logger.info("测试基本消息模型(简单队列)SimpleQueue---结束");
}
/**
* 测试 Direct-exchange模式
*/
@RequestMapping(value = "/directExchangeTest", method = RequestMethod.POST)
public void directExchangeTest() {
logger.info("测试 Direct-exchange模式 队列名为directQueue---开始");
for (int i = 0; i < 10; i++) {
directExchangeProduce.sendMessage();
}
logger.info("测试 Direct-exchange模式 队列名为directQueue---结束");
}
/**
* 测试 Fanout-exchange模式
*/
@RequestMapping(value = "/fanoutExchangeTest", method = RequestMethod.POST)
public void fanoutExchangeTest() {
logger.info("测试 fanout-exchange模式 队列名为fanoutQueue---开始");
fanoutExchangeProduce.sendMessage();
logger.info("测试 fanout-exchange模式 队列名为fanoutQueue---结束");
}
/**
* 测试 Topic-exchange模式 topicA 和 topicB
*/
@RequestMapping(value = "/topictExchangeTest", method = RequestMethod.POST)
public void topictExchangeTest() {
logger.info("测试 topict-exchange模式 队列名为topictQueueNameA---开始");
topicExchangeProduce.sendMessageTopicA();
logger.info("测试 topict-exchange模式 队列名为topictQueueNameA---结束");
logger.info("测试 topict-exchange模式 队列名为topictQueueNameB---开始");
topicExchangeProduce.sendMessageTopicB();
logger.info("测试 topict-exchange模式 队列名为topictQueueNameB---结束");
}
/**
* 测试 ack模式
*/
@RequestMapping(value = "/ackTest", method = RequestMethod.POST)
public void ackTest() {
logger.info("测试 ack 队列名为ackQueue---开始");
rabbitAckProduce.sendMessage();
logger.info("测试 ack 队列名为ackQueue---结束");
}
/**
* 测试 死信队列
*/
@RequestMapping(value = "/dealTest", method = RequestMethod.POST)
public void dealTest() {
logger.info("测试 死信队列---开始");
rabbitDealProduce.send();
logger.info("测试 死信队列---结束");
}
}
RabbitDealConsumer代码如下:
package com.example.rabbitmqconsumer.consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 死信队列 的消息消费者
* @RabbitListener(queues = "consumer.queue") 监听名为consumer.queue的队列
*/
@Component
@RabbitListener(queues = "consumer.queue")
public class RabbitDealConsumer {
@Autowired
private AmqpTemplate rabbitmqTemplate;
private Logger logger = LoggerFactory.getLogger(RabbitDealConsumer.class);
/**
* 消费消息
* @RabbitHandler 代表此方法为接受到消息后的处理方法
*/
@RabbitHandler
public void receiveMessage(Map msg){
logger.info("经过死信之后,消费者接收到的消息 :" + msg.toString());
}
}
首先启动生产者rabbitmq-produce项目。在postman或浏览器上访问:
http://localhost:8783/TestController/dealTest POST请求
这时可以在rabbitmq-produce的控制台可以看到
然后再启动消费者rabbitmq-consumer工程,在rabbitmq-consumer可以看到: