RabbitMQ 持久化 可靠性投递 可靠性消费(2)

默认情况下,exchange、queue、message 等数据都是存储在内存中的,这意味着如果 RabbitMQ 重启、关闭、宕机时所有的信息都将丢失。

RabbitMQ 提供了持久化来解决这个问题,持久化后,如果 RabbitMQ 发送 重启、关闭、宕机,下次起到时 RabbitMQ 会从硬盘中恢复exchange、queue、message 等数据。

持久化

RabbitMQ 持久化包含3个部分:

  • exchange 持久化,在声明时指定 durable 为 true
  • queue 持久化,在声明时指定 durable 为 true
  • message 持久化,在投递时指定 delivery_mode=2(1是非持久化)

queue 的持久化能保证本身的元数据不会因异常而丢失,但是不能保证内部的 message 不会丢失。要确保 message 不丢失,还需要将 message 也持久化

如果 exchange 和 queue 都是持久化的,那么它们之间的 binding 也是持久化的。

如果 exchange 和 queue 两者之间有一个持久化,一个非持久化,就不允许建立绑定。

注意:一旦确定了 exchange 和 queue 的 durable,就不能修改了。如果非要修改,唯一的办法就是删除原来的 exchange 或 queue 后,重现创建

拓展

如果将所有的消息都进行持久化操作,这样会严重影响 RabbitMQ 的性能。写入磁盘的速度可比写入内存的速度要慢很多。所以需要在可靠性和吞吐量之间做权衡。

将 exchange、queue 和 message 都进行持久化操作后,也不能保证消息一定不会丢失,消息存入RabbitMQ 之后,还需要一段时间才能存入硬盘。RabbitMQ 并不会为每条消息都进行同步存盘,如果在这段时间,服务器宕机或者重启,消息还没来得及保存到磁盘当中,就会丢失。对于这种情况,可以引入 RabbitMQ 镜像队列机制

代码实现

通过代码实现 RabbitMQ 持久化

原生的实现方式

原生的 RabbitMQ 客户端需要完成三个步骤。

第一步,设置交换器的持久化

// 三个参数分别为 交换器名、交换器类型、是否持久化
channel.exchangeDeclare(EXCHANGE_NAME, "topic", true);  

第二步,设置队列的持久化

// 参数1 queue :队列名  
// 参数2 durable :是否持久化  
// 参数3 exclusive :仅创建者可以使用的私有队列,断开后自动删除  
// 参数4 autoDelete : 当所有消费客户端连接断开后,是否自动删除队列  
// 参数5 arguments  
channel.queueDeclare(QUEUE_NAME, true, false, false, null); 

第三步,设置消息的持久化

// 参数1 exchange :交换器  
// 参数2 routingKey : 路由键  
// 参数3 props : 消息的其他参数,其中 MessageProperties.PERSISTENT_TEXT_PLAIN 表示持久化  
// 参数4 body : 消息体  
channel.basicPublish("", queue_name, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());  

RabbitMQ的工作模式消息的可靠性投递与消费

RabbitMQ的工作模式

RabbitMQ的工作模式,根据它的工作模式,一条消息从生产者发出,到消费者消费,需要经历以下4个步骤:

生产者将消息发送给RabbitMQ的Exchange交换机;

Exchange交换机根据Routing key将消息路由到指定的Queue队列;

消息在Queue中暂存,等待消费者消费消息;

消费者从Queue中取出消息消费。

通过这种工作模式,很好地做到了两个系统之间的解耦,并且整个过程是一个异步的过程,producer发送消息后就可以继续处理自己业务逻辑,不需要同步等待consumer的消费结果。

但任何一项技术的引入,除了带来它自身的优点之外,必然也会带来其他的一些缺点。MQ消息中间件虽然可以做到系统之间的解耦以及异步通信,但可能会存在消息丢失的风险。

该工作模式存在的问题——消息可能丢失

什么是消息丢失呢?简单来说,就是producer发送了一条消息出去,但由于某种原因(比如RabbitMQ宕机了),导致consumer没有消费到这条消息,最终导致producer与consumer两个系统的数据与期望结果不一致。

1.保证producer发送消息到RabbitMQ broker的可靠性

我们知道,producer发送消息到broker的过程中,丢失消息的原因是producer发送完消息之后,就主观认为消息发送成功了,即使RabbitMQ发生故障导致没有接收到消息,producer也是无法知道的。所以,要保证producer发出去的消息100%被broker接收成功,我们需要让producer发送消息后知道一个结果,然后根据这个结果再做相应的处理。

RabbitMQ提供了两种方式来达到这一目的:一种是Transaction(事务)模式,一种是Confirm(确认)模式。

channel 的 Transaction模式

Transaction模式类似于我们操作数据库的操作,首先开启一个事务,然后执行sql,最后根据sql执行情况进行commit或者rollback。

RabbitMQ 中与事务机制有关的方法有三个

  • txSelect:用于将当前 channel 设置成 transaction 模式
  • txCommit:用于提交事务
  • txRollback:用于回滚事务

在通过 txSelect 开启事务之后,我们便可以发布消息给broker代理服务器了,如果txCommit提交成功了,则消息一定到达了broker了,如果在 txCommit 执行之前 broker 异常崩溃或者由于其他原因抛出异常,这个时候我们便可以捕获异常通过 txRollback 回滚事务了

事务确实能够解决 producer 与 broker 之间消息确认的问题,只有消息成功被 broker 接收,事务提交才能成功,否则我们便可以在捕获异常进行事务回滚操作,同时进行消息重发,但是使用事务机制的话会降低RabbitMQ的性能。

try{
	channel.txSelect();
	
	channel.basicPublish("exchangeName","routingKey",false,null,message.getBytes())\
	int i = 1/0;  // 模拟broker 发生故障导致异常
	
	channel.txCommit();
}catch(Exception e){

	channel.txRollback();
}

Transaction模式虽然可以保证消息从producer到broker的可靠性投递,但它的缺点也很明显,它是阻塞的,只有当一条消息被成功发送到RabbitMQ之后,才能继续发送下一条消息,这种模式会大幅度降低RabbitMQ的性能,不推荐使用。

channel 的 Confirm模式(推荐)

RabbitMQ 提供了一个更好的方案,使用 channel 信道的 confirm 模式。

生产者通过调用 channel 的 confirmSelect 方法将 channel 设置为 confirm 模式.该模式下,所有在该信道上发布的消息都会被分派一个唯一的ID(从1开始),当消息被路由到所有匹配的Queue队列后,broker 就会发送一个(包含消息的唯一 ID 的)确认(ack)给producer发送端如果是持久化的消息,那么这个确认(ack)会在RabbitMQ将这条消息写入磁盘之后发出),这样producer就可以知道消息已经成功到达Queue队列了。 如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条不确认(nack)消息,nack消息中也会包含producer发布的消息唯一ID,producer接收到nack的消息之后,可以针对发布失败的消息做相应处理,比如重新发布等。

broker回传给发送端的确认消息中 deliver-tag 域包含了确认消息的ID,此外 broker 也可以设置 basic.ack 的 multiple 域,表示到这个ID之前的所有消息都已经得到了处理

confirm模式最大的好处在于他是异步的,生产者可以在等信道返回的同时继续发送下一条消息。

当消息被投递到所有匹配的队列后,broker 就会发送一个(包含消息的唯一 ID 的)确认给发送端”,万一发送确认后, rabbitMq 崩溃了,消息队列中的消息就都没了,这时候发送端还以为消息还在队列中。

为了防止这种情况的发送,rabbitMq 需要对队列和消息进行持久化。

当消息和队列开启持久化之后,确认信息会等到消息写入磁盘之后再发出

Confirm 的三种使用方式
  • 普通确认:每发送一条消息后,调用channel.waitForConfirms()方法,同步等待服务器端confirm。实际上是一种串行confirm了。

    channel.confirmSelect();
    channel.basicPublish("exchangeName","routingKey",false,null,message.getBytes());
    if(channel.waitForConfirms()){
    	// 消息发送成功
    }else{
    	// 消息确认识别,进行消息重发
    }
    

    这种方式实际上是一种同步等待的方式,只有当一条消息被确认之后,才能发送下一条消息,所以,实际使用中不推荐这种方式。

  • 批量确认:每发送一批消息后,调用channel.waitForConfirms()方法,同步等待服务器端confirm

    channel.confirmSelect();
    channel.basicPublish("exchange.Name","routingKey",false,null,message1.getBytes());
    channel.basicPublish("exchange.Name","routingKey",false,null,message2.getBytes());
    channel.basicPublish("exchange.Name","routingKey",false,null,message3.getBytes());
    
    channel.waitForConfirmsOrDie();
    

    waitForConfirmsOrDie()方法会等最后一条消息被确认或者得到nack时才会结束,这种方式虽然可以做到多条消息并行发送,不用互相等待,但最后确认的时候还是通过同步等待的方式完成的,所以也会造成程序的阻塞,并且当有任意一条消息未确认就会抛出异常,实际使用中不推荐这种方式。

  • 异步确认:为channel添加一个监听器,rabbitmq 会回调这个方法,示例代码如下

    channel.confirmSelect();
    
    for(int i = 0;i < 10; i++){
    	String msg = "confirmMessage:" + (i + 1);
    	channel.basicPublish("exhangeName","confirm",null,msg.getBytes(StandardCharsets.UTF_8));
    }
    
    // 异步监听确认和未确认的消息
    // 添加一个确认监听
    channel.addConfirmListener(new ConfirmListener() {
        //消息失败处理
        @Override
        public void handleNack(long deliveryTag, boolean multiple) throws IOException {
            //deliveryTag;唯一消息标签
            //multiple:是否批量
            System.err.println("-------no ack!-----------");
        }
        //消息成功处理
        @Override
        public void handleAck(long deliveryTag, boolean multiple) throws IOException {
            System.err.println("-------ack!-----------");
        }
    });
    

    异步确认的方式效率很高,多条消息既可以同时发送,不需要互相等待,又不用同步等待确认结果,只需异步监听确认结果即可,所以,实际使用中推荐使用这种方式。

2. 保证Exchange路由消息到Queue的可靠性

上面分析了,当producer发送消息时,由于小明手抖,导致消息的Routing key是一个不存在的key,从而变相丢失的情况,要如何提前规避掉呢?有两种方式:ReturnListener和使用备胎Exchange交换机。

(1)ReturnListener

ReturnListener是一个监听器,作用于Channel信道上,当producer发送一条消息给RabbitMQ后,如果由于Routing key不存在导致消息不可成功到达Queue队列,RabbitMQ就会将这条消息发送回producer的ReturnListener,在ReturnListener的handleReturn方法中,producer可以针对退回的消息做处理。

要使用ReturnListener,在发送消息时要注意,在basicPublish的方法中有一个mandatory的入参,只有将该参数值设置为true才可以正常使用ReturnListener,否则,当Routing key不存在时,消息会被自动丢弃。核心代码如下:

producer运行上述代码之后,就会打印出ReturnListener中的信息,此时,producer可以针对这条消息做业务处理,比如发送提醒信息给相关人员处理,或者更新状态等。但要注意,这里最好不要重发ReturnListener中的消息,因为导致消息被回退的原因就是消息不可达,如果在ReturnListener中重发这条消息的话,那么就有可能进入一个死循环,重发->退回->重发->退回......

(2)备胎Exchange交换机

除了使用ReturnListener,我们还可以使用备胎交换机的方式来解决Routing key不存在导致消息不可达的问题。所谓备胎交换机,是指当producer发送消息的Routing key不存在导致消息不可达时,自动将这条消息转发到另一个提前指定好的交换机上,这台交换机就是备胎交换机。备胎交换机也有自己绑定的Queue队列,当备胎交换机接到消息后,会将消息路由到自己匹配的Queue队列中,然后由订阅了这些Queue队列的消费者消费。

在开发时,如果要使用备胎交换机,也要在发送消息时,将mandatory参数值设置为true,否则,消息就会由于不可达而被RabbitMQ自动丢弃。核心代码如下:

然后我们运行该程序,然后可以在RabbitMQ控制台看到,消息被成功路由到了备胎交换机绑定的Queue队列:

然后我们开启一个消费者消费该Queue,也可以正常消费到这条原本不可达的消息

3. 保证Queue消息存储的可靠性

消息到达Queue队列之后,在消费者消费之前,RabbitMQ宕机也会导致消息的丢失,所以,为了解决这种问题,我们需要将消息设置成持久化的消息。持久化消息会写入RabbitMQ的磁盘中,RabbitMQ宕机重启后,会恢复磁盘中的持久化消息。

但消息是存储于Queue队列中的,所以,只把消息持久化也不行,也要把Queue也设置成持久化的队列,这样,RabbitMQ宕机重启之后,Queue才不会丢失,否则,即使消息是持久化的,但Queue不是持久化的,那么RabbitMQ重启之后,Queue都不存在了,那么消息也就无处存放,也就相当于没了。

4. 保证consumer消费消息的可靠性

consumer消费消息时,有一个ack机制,即向RabbitMQ发送一条ack指令,表示消息已经被成功消费,RabbitMQ收到ack指令后,会将消息从本地删除。默认情况下,consumer消费消息是自动ack机制,即消息只要到达consumer,就会向RabbitMQ发送ack,不管consumer是否消费成功。所以,为了保证producer与consumer数据的一致性,我们要使用手动ack的方式确认消息消费成功,即在消息消费完成后,通过代码显式调用发送ack。
channel.basicConsume 中 autoAck 设置为false。

首先,我们一起看下实现手动ack的核心代码:

consumer向RabbitMQ发送ack时有三种形式:

(1)reject:表示拒收消息。发送拒收消息时,需要设置一个 requeue 的参数,表示拒收之后,这条消息是否重新回到RabbitMQ的Queue之后,设置为true表示是,false表示否(消息会被删除)。若 requeue 设置为 true,那么消息回归原Queue之后,会被消费者重新消费,这样就会出现死循环,消费->拒绝->回Queue->消费->拒绝->回Queue......所以,一般设置为false。如果设置为true,那么最好限定消费次数,比如同一条消息消费5次之后就直接丢掉。

(2)nack:一般consumer消费消息出现异常时,需要发送nack给MQ,MQ接收到nack指令后,会根据发送nack时设置的requeue参数值来判断是否删除消息,如果requeue为true,那么消息会重新放入Queue队列中,如果requeue为false,消息就会被直接删掉。当requeue设置为true时,为了防止死循环性质的消费,最好限定消费次数,比如同一条消息消费5次之后就直接丢掉。

(3)ack:当consumer成功把消息消费掉后,需要发送ack给MQ,MQ接收到ack指令后,就会把消费成功的消息直接删掉。

补偿机制

经过上面这几个步骤的改造优化,我们的应用程序已经能够保证99.99%场景下消息的可靠性投递与消费了,但由于某些不可控因素,也并不能保证100%的消息可靠性,只有producer明确知道了consumer消费成功了,才能100%保证两边数据的一致性。

因为MQ是异步处理,所以producer是无法通过RabbitMQ知道consumer是否消费成功了,所以,如果要保证两边数据100%一致,consumer在消费完成之后,要给producer发送一条消息通知producer自己消费成功了。

但producer不能一直在那干等着,如果consumer过了1小时还没有发送消息给producer,那么很可能是consumer消费失败了,所以,producer与consumer之间要根据业务场景定义一个反馈超时时间,并在producer后台定义一个定时任务,定时扫描超过指定时间未接收到consumer确认的消息,然后重发消息。重发消息的逻辑中,最好定义一个重发最大次数,比如重发3次后还是不行的话,那可能就是consumer有bug或者发生故障了,就停止重发,等待问题查明再解决。

既然producer可能会重发消息,所以,consumer端一定要做幂等控制(就是已经消费成功的消息不再次消费),要做到幂等控制,消息体中就需要有一个唯一的标识,consumer可以根据这个唯一标识来判断自己是否已经成功消费了这条消息。

要保证 RabbitMQ 的消息可靠性投递,需要做到以下几点

  • 消息发送端开启 channel 的 confirm 模式
  • 消息发送端异步接收 RabbitMQ 响应
  • RabbitMQ 对队列和消息进行持久化
  • 消息发送端建立消息投递失败的补偿机制
posted @ 2023-01-06 10:42  caibaotimes  阅读(104)  评论(0编辑  收藏  举报