如何保证消息队列消息不丢失?

以RabbitMQ举例

消息的传递流程

消息队列如何取消_rabbitmq

  1. 生产者传递到Broker
  2. Broker之间传递消息
  3. 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宕机了,那么这条准备存入的消息就丢失了

怎么预防这种情况?

  1. 高可用性集群: 配置 RabbitMQ 为高可用性集群,确保有多个节点来处理消息。这样即使一个节点宕机,其他节点仍然可以继续处理消息。
  2. 备份与复制: 配置消息的备份与复制,确保消息被复制到多个节点。这样即使一个节点宕机,备份节点仍然有消息的拷贝。
  3. 镜像队列: RabbitMQ 提供了队列的镜像机制,允许将队列的消息复制到多个节点上,提高队列的可用性和可靠性。
  4. 消息确认机制: 使用消费者发送确认(ack)机制,确保消息在成功处理后才被确认。这可以防止在消息处理过程中发生错误导致消息丢失。

消费阶段不丢消息

消费者执行完业务逻辑,再给Broker返回ACK,这样才可以保证消费阶段不丢消息