之前公司是一个类电商公司,会有用户下单后未支付取消订单的场景,解决方案是使用RabbitMQ的死信队列来实现一个延时队列:下单时,将订单丢进消息队列,设置过期时间(订单失效时间),然后到时候检查订单状态,如果未支付则取消订单。
1.什么是死信
“死信”(Dead Letter)是RabbitMQ中的一种消息机制,当你在消费消息时,如果队列里的消息出现以下情况:
- 消息被否定确认,使用basic.reject 或者 basic.nack,并且此时requeue 属性被设置为false
- 消息在队列的存活时间超过设置的TTL(TimeToLive)时间
- 消息队列的消息数量已经超过最大队列长度
那么该消息将成为“死信”,如果配置了死信队列,那么该消息将会被丢进死信队列中,如果没有配置,则该消息将会被丢弃。死信交换机也是一种普通交换机,只不过是接受死信队列而已
2.如何实现死信队列
死信队列与普通队列一样有三种模式:Direct、Topic、Fanout
Direct:需要将一个队列绑定到交换机上,要求该消息与一个特定的路由键完全匹配。这是一个完整的匹配。
Topic:只需要简单的将队列绑定到交换机上。一个发送到交换机的消息都会被转发到与该交换机绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。Fanout交换机转发消息是最快的。
Fanout:将路由键和某模式进行匹配。此时队列需要绑定要一个模式上。符号“#”匹配一个或多个词,符号“*”匹配不多不少一个词。因此“abc.#”能够匹配到“abc.def.ghi”,但是“abc.” 只会匹配到“abc.def”。
实现死信队列步骤:
- 声明死信队列
- 声明死信交换机
- 绑定死信队列与死信交换机
- 声明业务队列(配置死信交换机和死信key)
- 声明业务交换机
- 绑定业务队列与业务交换机
这样当业务队列中的message变成dead letter时,就会被投递到死信队列中,然后可以通过监控死信队列,拿到业务message
就是这2个参数:
x-dead-letter-exchange:出现dead letter之后将dead letter重新发送到指定exchange
x-dead-letter-routing-key:出现dead letter之后将dead letter重新按照指定的routing-key发送
3.具体实现
新建Springboot项目,使用SpringAMQP+SpringWeb
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>RabbitMQDemo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml
#rabbitmq
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
#消息确认机制
#确认消息已发送到交换机(Exchange)
publisher-confirm-type: correlated
#确认消息未被接收时返回 而不是丢弃
publisher-returns: true
#确认消息
template:
mandatory: true
#消费端手动ack
listener:
simple:
acknowledge-mode: manual
配置消息
package com.example.demo.config;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class RabbitmqConfig {
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate();
rabbitTemplate.setConnectionFactory(connectionFactory);
//消息发送到exchange后回调
rabbitTemplate.setConfirmCallback((correlationData, b, s) -> {
System.out.println("ConfirmCallback: "+"相关数据:"+correlationData);
System.out.println("ConfirmCallback: "+"确认情况:"+b);
System.out.println("ConfirmCallback: "+"原因:"+s);
});
//从exchange发送到队列失败后回调
rabbitTemplate.setReturnsCallback((callback) -> {
System.out.println("ReturnCallback: "+"消息:"+callback.getMessage());
System.out.println("ReturnCallback: "+"回应码:"+callback.getReplyCode());
System.out.println("ReturnCallback: "+"回应信息:"+callback.getReplyCode());
System.out.println("ReturnCallback: "+"交换机:"+callback.getExchange());
System.out.println("ReturnCallback: "+"路由键:"+callback.getRoutingKey());
});
return rabbitTemplate;
}
/**
* 声明死信队列
*/
@Bean
public Queue deadQueue(){
return new Queue("dead-queue",true);
}
/**
* 声明死信交换机
*/
@Bean
public DirectExchange deadExchane(){
return new DirectExchange("dead-exchange");
}
/**
* 绑定死信队列到死信交换机
*/
@Bean
public Binding deadBing() {
return BindingBuilder.bind(deadQueue()).to(deadExchane()).with("dead");
}
/**
* 声明订单队列
*/
@Bean
public Queue orderQueue(){
Map<String, Object> map = new HashMap<>(3);
//死信交换机,对应上面的死信队列
map.put("x-dead-letter-exchange","dead-exchange");
//死信key,对应上面的死信key
map.put("x-dead-letter-routing-key","dead");
//可以在这里统一设置过期时间,也可以在发送消息到队列的时候设置
//map.put("x-message-ttl",5 * 1000);
return new Queue("order-queue",true,false,false,map);
}
/**
* 声明订单交换机
*/
@Bean
public DirectExchange orderExchane(){
return new DirectExchange("order-exchange");
}
/**
* 绑定订单队列到订单交换机
*/
@Bean
public Binding orderBinding() {
return BindingBuilder.bind(orderQueue()).to(orderExchane()).with("order");
}
}
监听死信队列,这里没有手动确认会一直在队列中
package com.example.demo.listener;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class DeadMessageReceiver {
@RabbitListener(queues = "dead-queue")
public void process(Message message, Channel channel, Map map) {
System.out.println("收到死信队列消息:" + message.toString());
}
}
发送消息
package com.example.demo.controller;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@RestController
public class SendMessageController {
@Autowired
RabbitTemplate rabbitTemplate;
@GetMapping("/sendOrderMessage")
public String sendOrderMessage() {
String messageId = String.valueOf(UUID.randomUUID());
String messageInfo = "Order message";
String time = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
Map map = new HashMap();
map.put("messageId",messageId);
map.put("messageInfo",messageInfo);
map.put("time",time);
//将消息发送到订单队列
rabbitTemplate.convertAndSend("order-exchange", "order",map,message -> {
message.getMessageProperties().setExpiration(5000 + "");
System.out.println("5秒后将过期");
return message;
});
System.out.println("发送消息成功");
return "success";
}
}
测试:
发送消息,5秒后过期到死信队列中
可以看到order-quueue的消息到dead-queue中去了
参考:
https://blog.csdn.net/qq_41389354/article/details/111352242