如何保证消息队列消息不丢失?
以RabbitMQ举例
消息的传递流程
- 生产者传递到Broker
- Broker之间传递消息
- Broker传递到消费者
要确保消息不丢失其实就是保证这三个过程中消息不会丢失
生产者到RabbitMQ不丢失
生产者到 RabbitMQ:事务机制和 Confirm 机制,注意:事务机制和 Confirm 机制是互斥的,两者不能共存,会导致 RabbitMQ 报错
事务消息机制(不推荐)
@Resource
private RabbitTemplate rabbitTemplate;
rabbitTemplate.setChannelTransacted(true);
@Bean
public RabbitTransactionManager rabbitTransactionManager(ConnectionFactory connectionFactory)
{
return new RabbitTransactionManager(connectionFactory);
}
@Transactional(rollbackFor = Exception.class,transactionManager = "rabbitTransactionManager")
public void publishMessage(String message) throws Exception {
rabbitTemplate.setMandatory(true);
rabbitTemplate.convertAndSend("javatrip",message);
}
事务消息机制由于会严重降低性能,所以一般不采用这种方法,因为这是同步操作,一条消息发送之后会使发送端阻塞,以等待RabbitMQ-Server的回应,之后才能继续发送下一条消息,生产者生产消息的吞吐量和性能都会大大降低
confirm消息确认机制
顾名思义,就是生产端投递的消息一旦投递到RabbitMQ后,RabbitMQ就会发送一个确认消息给生产端,让生产端知道我已经收到消息了,否则这条消息就可能已经丢失了,需要生产端重新发送消息了。
# 开启发送确认
spring.rabbitmq.publisher-confirm-type=correlated
# 开启发送失败回退
spring.rabbitmq.publisher-returns=true
@Configuration
@Slf4j
public class RabbitMQConfig {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void enableConfirmCallback() {
//confirm 监听,当消息成功发到交换机 ack = true,没有发送到交换机 ack = false
//correlationData 可在发送时指定消息唯一 id
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if(!ack){
//记录日志、发送邮件通知、落库定时任务扫描重发
}
});
//当消息成功发送到交换机没有路由到队列触发此监听
rabbitTemplate.setReturnsCallback(returned -> {
//记录日志、发送邮件通知、落库定时任务扫描重发
});
}
}
实际在这两个监听里面去做重发并不是很多,因为成本太高了,首先 RabbitMQ 本身丢失的可能性就非常低,其次如果这里需要落库再用定时任务扫描重发还要开发一堆代码,分布式定时任务…再其次定时任务扫描肯定会增加消息延迟,不是很有必要。真实业务场景是记录一下日志就行了,方便问题回溯,顺便发个邮件给相关人员,如果真的极其罕见的是生产者弄丢消息,那么开发往数据库补数据就行了
RabbitMQ确保消息不丢失
RabbitMQ为了确保消息的不丢失,为开发者提供了消息确认机制,以及消息的持久化机制
消息持久化
通过MessageProperties.PERSISTENT_TEXT_PLAIN 让消息持久化(保存在磁盘上), 防止在RabbitMQ突然宕机的情况下消息丢失问题
public class Producer {
public static final String QUEUE_NAME = "hello";
//发消息
public static void main(String[] args) throws Exception {
//创建连接工厂
Channel channel = MQUtil.getMQChannel();
/**
* 生成一个队列 参数说明
* 1.队列名称
* 2.队列中的消息是否进行持久化,是就存入磁盘,默认false存储在内存中
* 3.该队列是否只供一个消费者进行消费 是否进行消息队列共享
* 4.是否自动删除 最后一个消费者断开连接后 是否进行删除
*/
boolean durable = true;
// 将队列设置为持久化
channel.queueDeclare(QUEUE_NAME, durable, false, false, null);
// 发送消息
String message = "hello, world";
Scanner sc = new Scanner(System.in);
while (true) {
System.out.print("请输入要发送的消息:");
message = sc.next();
// MessageProperties.PERSISTENT_TEXT_PLAIN 让消息持久化(保存在磁盘上)
channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes(StandardCharsets.UTF_8));
System.out.println("发送成功");
}
}
}
在简单任务队列中,消息持久化确实足以应对消息丢失,但是消息持久化不能保证不会发生消息丢失,比如在在消息存入磁盘的过程中RabbitMQ宕机了,那么这条准备存入的消息就丢失了
怎么预防这种情况?
- 高可用性集群: 配置 RabbitMQ 为高可用性集群,确保有多个节点来处理消息。这样即使一个节点宕机,其他节点仍然可以继续处理消息。
- 备份与复制: 配置消息的备份与复制,确保消息被复制到多个节点。这样即使一个节点宕机,备份节点仍然有消息的拷贝。
- 镜像队列: RabbitMQ 提供了队列的镜像机制,允许将队列的消息复制到多个节点上,提高队列的可用性和可靠性。
- 消息确认机制: 使用消费者发送确认(ack)机制,确保消息在成功处理后才被确认。这可以防止在消息处理过程中发生错误导致消息丢失。
消费阶段不丢消息
消费者执行完业务逻辑,再给Broker返回ACK,这样才可以保证消费阶段不丢消息