RabbitMQ 消息可靠性
消息丢失的场景有哪些?
分析导致RabbitMQ消息丢失的场景,不妨先看看一条消息从生产者发送到消费者消费的整个过程。
从图中我们可以看到消息从生产到被消费的整个过程:
① 生产者发送消息给交换机。
② 交换机将消息路由到队列。
③ 消费者从队列中消费消息。
以上三步中的每一步都可能导致消息丢失。
1. 消息发送过程导致的消息丢失
生产者将消息发送给交换机时,可能由于网络波动等原因导致消息不能正确到达交换机,导致消息丢失。
解决方案:发送方确认机制,即消息正确到达交换机后,RabbitMQ向生产者发送确认消息
代码示例:
以 Spring Boot 整合 RabbitMQ 为例
注:关于 Spring Boot 整合 RabbitMQ 可以参考我的另一篇博文
添加如下配置:
# 开启发送方确认机制
spring.rabbitmq.publisher-confirms=true
创建一个Controller接口测试消息发送
public class RabbitMQController {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostMapping("/sendMessage")
public void sendMessage(){
// 设置确认回调方法
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if(ack){
log.info("消息发送成功,消息ID:{}", correlationData.getId());
}else {
log.info("消息发送失败, 消息ID:{}, 失败原因:{}", correlationData.getId(), cause);
}
});
String uuid = UUID.randomUUID().toString();
log.info("发送消息,ID:{}", uuid);
rabbitTemplate.convertAndSend( "test-routing-key", (Object) "this is a message", new CorrelationData(uuid));
}
}
在这里我们没有指定消息发送到的交换机,发送到 RabbitMQ 默认的名称为 amq.direct 的直流交换机。
使用postman测试接口:
可以看到回调方法被调用,消息成功达到交换机。
发送失败测试:
我们发送消息时指定一个不存在的交换机,模拟发送失败的情况
rabbitTemplate.convertAndSend("not-exists-exchange", "test-routing-key", (Object) "this is a message", new CorrelationData(uuid));
由于 RabbitMQ 服务器找不到对应的交换机,返回确认失败的信息(ack = false)。
tips:到目前为止我们还没有创建队列,以上进行的测试只是生产者发送消息到交换机的过程,只要消息成功到达交换机,无论是否有队列可以接收消息,都会返回成功确认信息。
2. 路由消息时导致的消息丢失
当消息到达交换机后,没有相应的队列与交换机绑定就会导致消息丢失。
添加配置:
# 当消息匹配到Queue并且失败时,会通过回调returnCallback方法返回消息
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.template.mandatory=true
设置找不到匹配队列时的回调方法:
rabbitTemplate.setReturnCallback((message, replyCode, replyText,
exchange, routingKey) -> {
log.info("return message: {}", new String(message.getBody()));
});
发送消息:
public void sendMessage(){
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if(ack){
log.info("消息发送成功,消息ID:{}", correlationData.getId());
}else {
log.info("消息发送失败, 消息ID:{}, 失败原因:{}", correlationData.getId(), cause);
}
});
rabbitTemplate.setReturnCallback((message, replyCode, replyText,
exchange, routingKey) -> {
log.info("消息路由失败: {}", new String(message.getBody()));
});
String uuid = UUID.randomUUID().toString();
log.info("发送消息,ID:{}", uuid);
rabbitTemplate.convertAndSend( "test-routing-key", (Object) "this is a message", new CorrelationData(uuid));
}
此时我们还没有创建绑定队列,交换机无法将消息投递到队列
图中可以看到,消息成功到达交换机,确认方法被回调,消息成功确认。另一方面,因为找不到对应的绑定队列,ReturnCallback 方法也被回调,表示消息路由失败。
3. 消费消息时导致消息丢失
默认情况下,消费者从队列中获取到消息后会直接确认签收,即自动签收,签收确认后消息会从队列中移除。倘若在消费者在处理收到的消息时发生宕机或者发生程序异常,就会导致消息的丢失。
消费者添加配置:
# 开启消费者手动确认
spring.rabbitmq.listener.simple.acknowledge-mode=manual
在 RabbitMQ 控制台创建名为"test-queue"的队列,并绑定到已经创建的"test-exchange"交换机,绑定键为"test-routing-key"。
消费者代码:
@Service
@Slf4j
@RabbitListener(queues = "test-queue")
public class RabbitConsumer {
/**
* 指定消费的队列
*/
@RabbitHandler
public void consume(String msg, Message message, Channel channel) throws IOException {
log.info("收到消息: {}", msg);
// 利用除0运行时异常模拟程序异常
int i = 2 / 0;
// 处理完毕,手动确认
log.info("处理完毕,发送确认");
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
这里我们利用除0运行时异常来模拟程序异常
启动生产者和消费者,postman调用发送消息的接口发送消息,消费者抛出运行时异常,通过控制台查看队列中的消息,如下:
图中可以看到消息仍然在队列中,没有被删除。
我们将除0的代码删除,使消息可以正常确认,重新启动消费者,可以看到消息被重新消费
再次查看队列,显示消息已经被删除了
4. 消息未持久化导致的消息丢失
RabbitMQ提供了交换机、队列、消息的持久化机制。当RabbitMQ宕机时保证数据不会丢失。
4.1 交换机和队列的持久化
我们创建的交换机和队列如何没有配置持久化,RabbitMQ宕机后这些交换机和队列就会丢失,导致消息无法正确地投递。
在 Spring Boot 整合 RabbitMQ 中,我们可以使用配置类创建交换机和队列,如下图:
这两个构造方法只传入名称,默认是持久化的。
4.2 消息的持久化
在 Spring Boot 中使用 rabbitTemplate 发送的消息是默认持久化的,因为其默认设置 delivery_model = 2 。
tips:
- 由于消息是依附于队列存在的,所以消息持久化必须首先要设置队列的持久化。
- 由于消息从内存写入磁盘需要时间,如果在此期间发生宕机,未来得及写入磁盘的消息会丢失。
持久化总结:
- 同时设置了队列和消息的持久化,RabbitMQ 服务重启后,队列和消息仍然存在。
- 仅设置队列的持久化,RabbitMQ 服务重启后,队列存在,队列中的消息会丢失。
- 仅设置消息的持久化,RabbitMQ 服务重启后, 由于队列消失,消息也就丢失了。