1 RabbitMQ 的持久化
1.1 场景导入
我在之前的博客提到过,当消费者消息接收到一半的时候挂掉时,可以使用消息接收确认机制进行补救。那在更加极端的场景下,RabbitMQ 挂掉了,又该怎么办呢?
在默认情况下,队列与消息都是非持久化的,想要将队列与消息进行保存,可以使用 RabbitMQ 的持久化机制。
1.2 理论原理
- RabbitMQ 所谓的持久化,就是将数据写在磁盘上
- 消息持久化分为三个部分,他们分别是:交换机持久化,队列持久化,消息持久化
- 如果交换机和队列都是持久化的,那么绑定也是持久化的,只要他们两个有一个是非持久化的,那么绑定也是非持久化的
- 队列与交换机的是否持久化在刚创建的时候就已经确定且不能更改,修改队列与交换机的是否持久化标志唯有删除信息并重建一个新的
1.3 队列持久化
如果队列不设置持久化 ,那么 RabbitMQ 服务重启后,相关的队列数据将会丢失。由于消息是存储在队列中的,所以队列中的消息也会丢失。
// 队列持久化代码
// 参数1 name :队列名
// 参数2 durable :是否持久化
// 参数3 exclusive :仅创建者可以使用的私有队列,断开后自动删除
// 参数4 autoDelete : 当所有消费客户端连接断开后,是否自动删除队列
new Queue(name, durable, exclusive, autoDelete);
1.4 交换机持久化
如果交换器不设置持久化,那么 RabbitMQ 服务重启后,相关的交换器数据将会丢失,不过消息不会丢失,只是 RabbitMQ 生产者不能正常发送消息。
// 交换机持久化代码
// 参数1 name :交互器名
// 参数2 durable :是否持久化
// 参数3 autoDelete :当所有消费客户端连接断开后,是否自动删除队列
new TopicExchange(name, durable, autoDelete)
1.5 消息持久化
我们都知道,队列的持久化只能保证队列本身的数据不会丢失,如果需要保证消息不会丢失则需要消息本身被持久化。
至于消息持久化的设置,由于我们通常使用 rabbitTemplate.convertAndSend(exchange, routeKey, message);
来发送消息,而这种方法已经默认消息是持久化的,所以一般我们不需要设置。
2 死信队列
2.1 简介
死信队列,又称 dead-letter-exchange(DLX)。当一条消息在一个队列中变成死信后,它会被重新发布到一个交换机中,这个交换机就是 DLX。
2.2 消息变成死信的几种情况
- 消息被拒绝(reject ,nack),并且 requeue = false(不再重新投递)
- 消息 TTL 过期
- 队列超过最长长度
2.3 实战
配置类
package com.example.consumer.config;
import java.util.HashMap;
import java.util.Map;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
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;
/**
* 直连型交换机
* @author 30309
*
*/
@Configuration
public class DirectRabbitConfig {
//队列,名称为DirectQueue
//同时将DirectQueue绑定到死信队列交换机上
@Bean
Queue DirectQueue() {
Map<String, Object> args = new HashMap<>(2);
//交换机标识符
args.put("x-dead-letter-exchange", "DeadExchange");
//绑定键标识符
args.put("x-dead-letter-routing-key", "DeadRoutingKey");
Queue queue = new Queue("DirectQueue", true, false, false, args);
return queue;
}
//直连型交换机,名称为DirectExchange
@Bean
DirectExchange DirectExchange() {
return new DirectExchange("DirectExchange");
}
//将队列和交换机绑定, 并设置用于匹配键:DirectRouting
@Bean
Binding bindingDirect() {
return BindingBuilder.bind(DirectQueue()).to(DirectExchange()).with("DirectRouting");
}
//创建死信队列
@Bean
Queue DeadQueue() {
return new Queue("DeadQueue", true);
}
//创建死信交换机
@Bean
DirectExchange DeadExchange() {
return new DirectExchange("DeadExchange");
}
//死信队列与死信交换机绑定
@Bean
Binding bindingDead() {
return BindingBuilder.bind(DeadQueue()).to(DeadExchange()).with("DeadRoutingKey");
}
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
// 消息发送失败返回到队列中, 配置文件需要配置 publisher-returns: true
rabbitTemplate.setMandatory(true);
return rabbitTemplate;
}
}
生产者:
package com.example.provider.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;
/**
* 生产者
* @author 30309
*
*/
@RestController
public class SendMessageController{
@Autowired
RabbitTemplate rabbitTemplate;
@GetMapping("/sendDirectMessage")
public String sendDirectMessage() {
//将消息携带绑定键值DirectRouting发送到交换机DirectExchange
rabbitTemplate.convertAndSend("DirectExchange", "DirectRouting", "Hello World");
return "ok";
}
}
消费者1,连接普通队列,我们设置让它直接拒绝消息
package com.example.consumer.receiver;
import java.io.IOException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import com.rabbitmq.client.Channel;
/**
* 消费者1
* @author 30309
*
*/
@Component
public class DirectReceiver1 {
@RabbitListener(queues = "DirectQueue")
@RabbitHandler
public void process(String str,Channel channel, Message message) {
System.out.println("DirectReceiver1消费者收到消息: " + str );
try {
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
} catch (IOException e) {
e.printStackTrace();
}
}
}
消费者2,我们让它连接死信队列,可以发现它收到被消费者1拒绝的消息
package com.example.consumer.receiver;
import java.io.IOException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import com.rabbitmq.client.Channel;
/**
* 消费者2
* @author 30309
*
*/
@Component
public class DirectReceiver2 {
@RabbitListener(queues = "DeadQueue")
@RabbitHandler
public void process(String str,Channel channel, Message message) {
System.out.println("DirectReceiver2消费者收到消息: " + str );
try {
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (IOException e) {
e.printStackTrace();
}
}
}
测试结果如下
结果正常
2.4 总结
- DLX 其实也是正常的交换机,能在任何队列上被指定
- 当队列中有死信时,RabbitMQ 会自动将消息重新发布到设置的交换机上去,进而被路由到另一个队列