概念
与普通队列的区别是,在延迟队列中的消息不会被消费者立马消费,而是延迟一段时间后,才会被消费。
场景
用户选择完商品后,提交订单。订单生成之后,在规定时间内,检验订单的支付状态,如果订单超过规定时间仍然没有支付,那么需要关闭此订单。
原理
用到了RabbitMQ的两个特性:Time-To-Live Extensions、Dead Letter Exchange。
Time-To-Live Extensions
TTL。TTL是RabbitMQ中一个消息或者队列的属性,表明一条消息或者该队列中的所有消息的最大存活时间。如果一条消息设置了TTL属性或者进入了设置TTL属性的队列,那么这条消息如果在TTL设置的时间内没有被消费,则会成为“死信”。如果同时配置了队列的TTL和消息的TTL,那么较小的那个值将会被使用。
在声明队列时设置
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-message-ttl", 60000);
channel.queueDeclare("myqueue", false, false, false, args);
设置每条消息的ttl
byte[] messageBodyBytes = "Hello, world!".getBytes();
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.expiration("60000")
.build();
channel.basicPublish("my-exchange", "routing-key", properties, messageBodyBytes);
Dead Letter Exchange
DLX。队列中的消息有可能是“死信”,当下面的情况发生的时候,消息就被认为是“死信”。
1、The message is negatively acknowledged by a consumer using basic.reject or basic.nack with requeue parameter set to false.
2、The message expires due to per-message TTL; or
3、The message is dropped because its queue exceeded a length limit.
设置方式
channel.exchangeDeclare("some.exchange.name", "direct");
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-dead-letter-exchange", "some.exchange.name");
channel.queueDeclare("myqueue", false, false, false, args);
在对消息进行死信处理的时候,还可以指定一个routing key。如果不指定,将使用消息本身的routing key。
args.put("x-dead-letter-routing-key", "some-routing-key");
指定后,在RabbitMQ管理界面,队列名称右边,会看到DLK特征。
Demo
接下来,我们将使用Spring Boot来进行演示,其中涉及的Exchange以及Queue的总体设计如下图:
引入依赖包
<!-- RabbitMQ -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
RabbitMQ配置
spring:
# RabbitMQ
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
listener:
type: simple
simple:
default-requeue-rejected: false
acknowledge-mode: none
ExchangeConfig
/**
* <p>交换机配置类</p>
*
*/
@Configuration
public class ExchangeConfig {
public static final String DL_MESSAGE_BUSINESS_EXCHANGE = "dl.message.business.exchange";
public static final String DL_MESSAGE_DEAD_LETTER_EXCHANGE = "dl.message.dead.letter.exchange";
// 业务Exchange
@Bean("dlMessageBusinessExchange")
public TopicExchange dlMessageOutpushBusinessExchange(){
TopicExchange topicExchange = new TopicExchange(DL_MESSAGE_BUSINESS_EXCHANGE,false,false);
return topicExchange;
}
// 死信Exchange
@Bean("dlMessageDeadLetterExchange")
public DirectExchange dlMessageOutpushDeadletterExchange(){
DirectExchange directExchange = new DirectExchange(DL_MESSAGE_DEAD_LETTER_EXCHANGE);
return directExchange;
}
}
QueueConfig
/**
* <p>队列配置类</p>
*
*/
@Configuration
public class QueueConfig {
public static final String DL_MESSAGE_BUSINESS_QUEUE = "dl.message.business.queue";
public static final String DL_MESSAGE_DEAD_LETTER_QUEUE = "dl.message.dead.letter.queue";
public static final String DL_MESSAGE_DEAD_LETTER_ROUTING = "dl.message.dead.letter";
@Bean("dlMessageBusinessQueue")
public Queue dlMessageOutpushBusinessQueueChaotao(){
Map<String, Object> args = new HashMap<>();
args.put("x-dead-letter-exchange", ExchangeConfig.DL_MESSAGE_DEAD_LETTER_EXCHANGE);
args.put("x-dead-letter-routing-key", DL_MESSAGE_DEAD_LETTER_ROUTING);
Queue queue = QueueBuilder.durable(DL_MESSAGE_BUSINESS_QUEUE).withArguments(args).build();
return queue;
}
@Bean("dlMessageDeadLetterQueue")
public Queue dlMessageOutpushDeadLetterQueueChaotao(){
Queue queue = new Queue(DL_MESSAGE_DEAD_LETTER_QUEUE);
return queue;
}
}
RabbitMqConfig
@Configuration
public class RabbitMqConfig {
/**
* Bindings Key
*/
public static final String DL_MESSAGE_BUSINESS_BINDING = "dl.message.business.#";
public static final String DL_MESSAGE_DEAD_LETTER_BINDING = "dl.message.dead.letter";
@Value("${spring.rabbitmq.host}")
private String host;
@Value("${spring.rabbitmq.port}")
private int port;
@Value("${spring.rabbitmq.username}")
private String username;
@Value("${spring.rabbitmq.password}")
private String password;
@Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory(host, port);
connectionFactory.setUsername(username);
connectionFactory.setPassword(password);
connectionFactory.setVirtualHost("/");
return connectionFactory;
}
@Bean
public AsyncRabbitTemplate asyncRabbitTemplate(RabbitTemplate rabbitTemplate){
return new AsyncRabbitTemplate(rabbitTemplate);
}
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public RabbitTemplate rabbitTemplate() {
RabbitTemplate template = new RabbitTemplate(connectionFactory());
return template;
}
@Bean
public Binding businessBinding(@Qualifier("dlMessageBusinessQueue") Queue queue,
@Qualifier("dlMessageBusinessExchange") TopicExchange exchange){
Binding binding = BindingBuilder.bind(queue).to(exchange).with(DL_MESSAGE_BUSINESS_BINDING);
return binding;
}
@Bean
public Binding deadLetterBinding(@Qualifier("dlMessageDeadLetterQueue") Queue queue,
@Qualifier("dlMessageDeadLetterExchange") DirectExchange exchange){
Binding binding = BindingBuilder.bind(queue).to(exchange).with(DL_MESSAGE_DEAD_LETTER_BINDING);
return binding;
}
}
消费者
@Component
public class TestRabbitMQListener {
private final Logger logger = LoggerFactory.getLogger(TestRabbitMQListener.class);
@RabbitListener(queues = QueueConfig.DL_MESSAGE_DEAD_LETTER_QUEUE)
public void receiveDeadLetter(Message message, Channel channel) throws IOException {
System.out.println("receive dead letter=" + new String(message.getBody()));
}
}
编写一个Rest接口,用于向队列中发送消息
@RestController
public class TesRabbitMQController {
@Autowired
private RabbitTemplate rabbitTemplate;
//routing key
public static final String DL_MESSAGE_BUSINESS_ROUTING_KEY = "dl.message.business.test.xxx";
@GetMapping("send")
public String sendMsg(@RequestParam("msg") String msg){
MessageProperties messageProperties = new MessageProperties();
//设置单条消息的超时时间为10秒
messageProperties.setExpiration("10000");
Message message = new Message(msg.getBytes(), messageProperties);
rabbitTemplate.convertAndSend(ExchangeConfig.DL_MESSAGE_BUSINESS_EXCHANGE, DL_MESSAGE_BUSINESS_ROUTING_KEY, message);
return "ok";
}
}
测试
以上代码编写完成后,启动我们的应用,会看见创建了两个Exchange
两个Queue
调用Rest接口后,会往延时队列中进行发送消息。
由于消息设置了10秒超时,所以10秒之后,此消息会进入死信队列。
我们的消费者监听死信队列,最终会在控制台打印出如下信息
receive dead letter=test