都说好记性不如烂笔头,每天写一点,从量变到质变的开始!废话不多说,以下所有内容均来自本人实际操作:
周末花了点时间,重温了一下RabbitMQ,在此记录一下实际项目中(实时消息/延迟消息/死信消息)的基本使用,如果想了解更多可以查看官网教程,本文为Spring Boot项目,因此参考了Spring Boot官方文档.
1.准备一个RabbitMQ账号(用户名=test/密码=test/virtual-host=/test,并授予管理员权限)
RabbitMQ的安装(略)
2.项目pom文件中导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
Spring Boot版本为2.4.1
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
3.项目yml配置文件中配置rabbitmq连接信息
spring:
rabbitmq:
host: 127.0.0.1
port: 5672
virtual-host: /test
username: test
password: test
#确认机制,唯一
publisher-confirm-type: correlated
#监听器默认使用simple,可以访问https://docs.spring.io/spring-amqp/docs/current/reference/html/#containerAttributes查看详细介绍
listener:
simple:
#手动确认模式
acknowledge-mode: manual
#每个使用者上未确认的最大消息数
prefetch: 1
#默认情况下,拒绝的传递是否重新排队
defaultRequeueRejected: false
# listener:
# type: direct
# direct:
# #手动确认模式
# acknowledge-mode: manual
# #每个使用者上未确认的最大消息数
# prefetch: 1
4.创建一个RabbitMQConfig配置类
交换机类型统一使用topic类型,因为topic交换机发信息时路由键为空("")相当于fanout,路由键单个单词写死(不包含./*/#)相当于direct
@Configuration
public class RabbitMQConfig {
/**
* 定义交换机
*/
public static final String DELAY_EXCHANGE = "springboot_delay_exchange";
public static final String DEAD_LETTER_EXCHANGE = "springboot_dead_letter_exchange";
public static final String TOPIC_EXCHANGE = "springboot_topic_exchange";
/**
* 定义队列
*/
public static final String DELAY_QUEUE = "springboot_queue_delay";
public static final String DEAD_LETTER_QUEUE = "springboot_queue_dead_letter";
public static final String QUEUE_A = "springboot_queue_a";
/**
* 定义路由规则
*/
public static final String DELAY_ROUTING_KEY = "springboot_delay.#";
public static final String DEAD_LETTER_ROUTING_KEY = "springboot_dead_letter.#";
public static final String QUEUE__ROUTING_KEY = "springboot.#";
延迟消息
/**
* 自定义交换机-作为延迟交换机
* 要使用“延迟消息交换”,您只需要声明一个提供"x-delayed-message"交换类型的交换
* https://www.rabbitmq.com/blog/2015/04/16/scheduling-messages-with-rabbitmq/
*
* @return
*/
@Bean
public CustomExchange delayExchange() {
Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "topic");
CustomExchange delayExchange = new CustomExchange(DELAY_EXCHANGE, "x-delayed-message", true, false, args);
return delayExchange;
}
/**
* 声明一个延时队列,并绑定到自定义的死信交换机上,此处不设置队列自身和消息的过期时间(这样做的话消息延迟时间将被写死),我们统一在发送消息的时候自己设置消息的延迟时间
* 消息流转过程:生产者->延时交换机(可以是普通交换机)->延时队列->死信交换机(DLX)->死信队列->消费者
* 这里的延迟意味着:将消息路由到队列或其他交换机的延迟
*
* @return
*/
@Bean
public Queue delayQueue() {
Map<String, Object> args = new HashMap<>(2);
// x-dead-letter-exchange 这里声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
// x-dead-letter-routing-key 这里声明当前队列的死信路由key
args.put("x-dead-letter-routing-key", DEAD_LETTER_ROUTING_KEY);
// "x-message-ttl"定义消息的过期时间单位毫秒
args.put("x-message-ttl", 6000);
//x-max-length 定义该队列最多接收消息数
args.put("x-max-length", 6);
return new Queue(DELAY_QUEUE, true, false, false, args);
}
/**
* 把延时队列绑定到延时交换机(可以是普通交换机)
*
* @return
*/
@Bean
public Binding bindingDelayQueue() {
return BindingBuilder.bind(delayQueue()).to(delayExchange()).with(DELAY_ROUTING_KEY).noargs();
}
死信消息
/**
* 声明一个交换机-作为死信交换机
* 死信交换(DLX)是普通交换,它们可以是任何通常的类型,并且可以照常声明.
*
* @return
*/
@Bean
public TopicExchange deadLetterExchange() {
return new TopicExchange(DEAD_LETTER_EXCHANGE);
}
/**
* 声明一个死信队列(实际业务需要监听该队列)
* 消息流转过程:生产者->延时交换机(可以是普通交换机)->延时队列->死信交换机(DLX)->死信队列->消费者
*
* @return
*/
@Bean
public Queue deadLetterQueue() {
return new Queue(DEAD_LETTER_QUEUE);
}
/**
* 把死信队列(实际业务需要监听该队列)绑定到死信交换
*
* @return
*/
@Bean
public Binding bindingDeadLetterQueue() {
return BindingBuilder.bind(deadLetterQueue()).to(deadLetterExchange()).with(DEAD_LETTER_ROUTING_KEY);
}
实时消息
/**
* 声明一个主题交换机,默认持久化,不自动删除
*
* @return
*/
@Bean
public TopicExchange topicExchange() {
return new TopicExchange(TOPIC_EXCHANGE);
}
/**
* 声明一个队列,默认持久化,不自动删除
*
* @return
*/
@Bean
public Queue springbootQueueA() {
return new Queue(QUEUE_A);
}
/**
* 把队列绑定到交换机并指定路由规则
*
* @return
*/
@Bean
public Binding bindingA() {
return BindingBuilder.bind(springbootQueueA()).to(topicExchange()).with(QUEUE__ROUTING_KEY);
}
}
5.为了方便测试,引入openapi和lombok依赖
<!-- swagger部分:用于自动生成和显示接口文档 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.5.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
6.创建生产者
@Slf4j
@RestController
@RequestMapping(value = "rabbitmq")
@Tag(name = "rabbitmq演示controller", description = "手动控制消息发送时机,查看消息接收情况")
public class Producer {
@Autowired
public RabbitTemplate rabbitTemplate;
private void sendDelayMsg(String exchange, String routingKey, Object message, Integer delayTime) {
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend(exchange, routingKey, message, a -> {
a.getMessageProperties().setDelay(delayTime);
return a;
}, correlationData);
log.info("发送了消息---" + message + "---消息ID---" + correlationData.getId());
}
@GetMapping("sendMsg")
@Operation(summary = "发送消息接口", description = "默认发送消息到springboot_topic_exchange交换机")
@Parameter(name = "msg", description = "发送的消息", example = "this is a real time message")
@Parameter(name = "routingKey", description = "消息的路由规则", example = "springboot.key1")
public ResponseEntity<String> sendMsg(@RequestParam(name = "msg", defaultValue = "this is a real time message") String msg,
@RequestParam(name = "routingKey", defaultValue = "") String routingKey) {
try {
this.sendDelayMsg(TOPIC_EXCHANGE, routingKey, msg, 0);
return ResponseEntity.ok(msg);
} catch (AmqpException e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
}
@GetMapping("sendTTLMsg")
@Operation(summary = "发送延迟消息接口", description = "默认发送消息到springboot_delay_exchange交换机")
@Parameter(name = "msg", description = "发送的消息", example = "this is a delay message")
@Parameter(name = "delayTime", description = "消息延迟多长时间(毫秒)发送", example = "10000")
@Parameter(name = "routingKey", description = "消息的路由规则", example = "springboot_delay.key1")
public ResponseEntity<String> sendTTLMsg(@RequestParam(name = "msg", defaultValue = "this is a delay message") String msg,
@RequestParam(name = "delayTime", defaultValue = "5000") Integer delayTime,
@RequestParam(name = "routingKey", defaultValue = "springboot_delay.") String routingKey) {
try {
this.sendDelayMsg(DELAY_EXCHANGE, routingKey, msg, delayTime);
return ResponseEntity.ok(msg);
} catch (AmqpException e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
}
}
7.创建消费者
@Slf4j
@Component
public class Consumer {
/**
* 接收普通队列消息
*/
@RabbitListener(queues = QUEUE_A)
public void processMessage(Message message, Channel channel, @Headers Map<String, Object> headers) throws IOException {
try {
//每次处理一条消息
channel.basicQos(1);
String msg = new String(message.getBody(), "UTF-8");
log.info("接收到普通队列的消息:---{}---消息ID---{}", msg, headers.get("spring_returned_message_correlation"));
/**
* 第一个参数,消息的标识
* RabbitMQ 推送消息给 Consumer 时,会附带一个 Delivery Tag,以便 Consumer可以在消息确认时告诉 RabbitMQ 到底是哪条消息被确认了;
* 第二个参数 multiple 取值为 false 时,表示通知 RabbitMQ 当前消息被确认;
* 如果为 true,则额外将比第一个参数指定的 delivery tag 小的消息一并确认。
*/
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
/**
* 第一个参数,消息的标识
* 第二个参数是否批量处理消息,true:将一次性拒绝所有小于deliveryTag的消息。
* 第三个参数,消息是否重入队列,false将消息存队列删除。true:会重复消费该消息直到消息被确认消费
*/
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
/**
* 接收延迟消息
*/
@RabbitListener(queues = DELAY_QUEUE)
public void processMessage1(Message message, Channel channel, @Headers Map<String, Object> headers) throws IOException {
try {
String msg = new String(message.getBody(), "UTF-8");
log.info("接收到延迟消息:---{}---消息ID---{}", msg, headers.get("spring_returned_message_correlation"));
} catch (UnsupportedEncodingException e) {
//第二个false将消息存队列删除,true放回队列
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
/**
* 接收死信队列消息
*/
@RabbitListener(queues = DEAD_LETTER_QUEUE)
public void processMessage2(Message message, Channel channel, @Headers Map<String, Object> headers) throws IOException {
try {
//可以考虑数据库记录,每次进来查数量,达到一定的数量,进行预警,人工介入处理
String msg = new String(message.getBody(), "UTF-8");
log.info("接收到死信消息:---{}---消息ID---{}", msg, headers.get("spring_returned_message_correlation"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
//回复ack
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
8.启动项目,访问openapi接口页面发消息测试
8.1 发送普通消息
8.2 发送延迟消息
8.3 发送死信消息
要想收到死信消息,必须使消息死亡然后通过死信交换机转移到死信队列,修改代码重启项目测试
发送了两条消息,可以看到第二条消息直接转移到了死信队列,并且时间间隔变成了8秒(消息自身2s+队列6s),第一条消息死亡后还在原队列(因为消费者还在)