RabbitMQ 消息可靠性投递分析

可靠性投递

可靠性只是问题的一个方面,发送消息的效率同样是我们需要考虑的问题,而这两个因素是无法兼得的。如果在发送消息的每一个环节都采取相关措施来保证可靠性,势必会对消息的收发效率造成影响。

例如:一些业务实时一致性要求不是特别高的场合,可以牺牲一些可靠性来换取效率。比如发送通知或者记录日志的这种场景,如果用户没有收到通知,不会造成很大的影响,就不需要严格保证所有的消息都发送成功。如果失败了,只要再次发送就可以了。

在代码里面一定是先操作数据库再发送消息。避免因为数据库回滚导致的数据不一致。但是如果先操作数据,后发送消息,发送消息出了问题,那不是一样会出现业务数据的不一致?所以,这又是一个经典的面试题:在使用 MQ 实现异步通信的过程中,有消息丢了怎么办?或者MQ消息重复了怎么办?

下面根据 RabbitMQ 的工作模型,来分析一下 RabbitMQ 为我们提供了哪些可靠性措施。
在这里插入图片描述

在我们使用 RabbitMQ 收发消息的时候,有几个主要环节:

① 代表消息从生产者发送到 Broker

生产者把消息发到 Broker 之后,怎么知道自己的消息有没有被 Broker 成功接收?如果 Broker 不给应答,生产者不断地发送,那有可能是一厢情愿,消息全部进了黑洞。

② 代表消息从 Exchage 路由到 Queue

Exchange 是一个绑定列表,它的职责是分发消息。如果它没有办法履行它的职责怎么办?也就是说,找不到队列或者找不到正确的队列,怎么处理?

③ 代表消息在 Queue 中存储

队列有自己的数据库( Mnesia ),它是真正用来存储消息的。如果还没有消费者来消费,那么消息要一直存储在队列里面。你的信件放在邮局,如果邮局内部出了问题,比如起火,信件肯定会丢失。怎么保证消息在队列稳定地存储呢?

④ 代替消费者订阅 Queue 并消费消息

队列的特性是什么? FIFO(先进先出)。队列里面的消息是一条一条的投递的,也就是说,只有上一条消息被消费者接收以后,才能把这一条消息从数据库删掉,继续投递下一条消息。

或者反过来说,如果消费者不签收,我是不能去派送下一个快件的,总不能丢在门口就跑吧?

问题来了,Broker(快递总部)怎么知道消费者已经接收了消息呢?下面我们就从这四个环节入手,分析如何保证消息的可靠性。

一、消息发送到 RabbitMQ 服务器(服务端确认 ACK )

第一个环节是生产者发送消息到 Broker。先来说一下什么情况下会发送消息失败?

可能因为网络连接或者Broker的问题((比如硬盘故障、硬盘写满了)导致消息发送失败,生产者不能确定 Broker 有没有正确的接收。

在 RabbitMQ 里面提供了两种机制服务端确认机制,也就是在生产者发送消息给 RabbitMQ 的服务端的时候,服务端会通过某种方式返回一个应答,只要生产者收到了这个应答,就知道消息发送成功了。

  • 第一种是 Transaction(事务)模式
  • 第二种 Confirm(确认)模式

1.1 Transaction(事务)模式

事务模式怎么使用呢?它在创建 channel 的时候,可以把信道设置成事务模式,然后就可以发布消息给 RabbitMQ 了。如果channel.txCommit();的方法调用成功,就说明事务提交成功,则消息一定到达了 RabbitMQ 中。

如果在事务提交执行之前由于 RabbitMQ 异常崩溃或者其他原因抛出异常,这个时候我们便可以将其捕获,进而通过执行channel.txRollback()方法来实现事务回滚。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a4Cfc7cp-1648845117610)(D:\soft_install\work_install\typoraImages\image-20220401013554860.png)]

在事务模式里面,只有收到了服务端的Commit-OK的指令,才能提交成功。所以可以解决生产者和服务端确认的问题。但是事务模式有一个缺点,它是阻塞的,一条消息没有发送完毕,不能发送下一条消息,它会榨干 RabbitMQ 服务器的性能。所以不建议大家在生产环境使用。

事务确认开启示例如下:

//设置事务模式
rabbitTemplate.setChannelTransacted(true);

1.2 Confirm(确认)模式

确认模式有三种:

  • 普通确认模式:发送1条确认1条
  • 批量确认模式:多条一起确认,有确认数量和多条确认中一条失败重发问题
  • 异步确认模式:可以一边发送一边确认

批量确认模式开启如下:

rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback(){
    public void returnedMessage(Message message,
                                int replyCode,
                                String replyText,
                                String exchange,
                                String routingKey){
        System.out.println("回发的消息:");
        System.out.println("replyCode: "+replyCode);
        System.out.println("replyText: "+replyText);
        System.out.println("exchange: "+exchange);
        System.out.println("routingKey: "+routingKey);
    }
});

异步确认开启如下:

//设置异步确认模式
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (!ack) {
            System.out.println("发送消息失败:" + cause);
            throw new RuntimeException("发送异常:" + cause);
        }
    }
});

二、消息从交换机路由到队列

以下情况可能会导致消息无法路由到正确的队列(但是生产环境基本上不会出现这两种问题):

  • routingkey 错误
  • 队列不存

可以用两种方式处处理无法路由的消息

  • 服务端重发给生产者
  • 让交换机路由到另一个备份的交换机

三、消息再队列存储

第三个环节是消息在队列存储,如果没有消费者的话,队列一直存在在数据库中。

如果 RabbitMQ 的服务或者硬件发生故障,比如系统宕机、重启、关闭等等,可能会导致内存中的消息丢失,所以我们要把消息本身和元数据(队列、交换机、绑定)都保存到磁盘。

解决方案:

1、队列持久化

2、交换机持久化

3、消息持久化

4、集群

四、消息投递到消费者

如果消费者收到消息后没来得及处理即发生异常,或者处理过程中发生异常,会导致④失败。服务端应该以某种方式得知消费者对消息的接收情况,并决定是否重新投递这条消息给其他消费者。
RabbitMQ提供了消费者的消息确认机制(message acknowledgement),消费者可以自动或者手动地发送ACK给服务端。

没有收到 ACK 的消息,消费者断开连接后,RabbitMQ 会把这条消息发送给其他消费者。如果没有其他消费者,消费者重启后会重新消费这条消息,重复执行业务逻辑。

消费者通过两种方式给 Broker 应答:

  • 自动 ACK(默认)
  • 手动 ACK

设置手动 ACK 在 SimpleRabbitListenerContainer 或者 SimpleRabbitListenerContainerFactory

factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);

注意这三个值的区别:

  • NONE:自动 ACK
  • MANUAL:手动 ACK
  • AUTO:如果方法未抛出异常,则发送ack。如果方法抛出异常,并且不是 AmqpRejectAndDontRequeueException 则发送 nack ,并且重新入队列。如果抛出异常时 AmqpRejectAndDontRequeueException 则发送 nack 不会重新入队列。

如果消费出现问题,确实是不能发送ACK告诉服务端成功消费了,可以使用拒绝消息的指令,而且还可以让消息重新入队给其他消费者消费。

如果消息无法处理或者消费失败,也有两种拒绝的方式,Basic.Reject()拒绝单条,Basic.Nack()批量拒绝。

下面是消费者调用 ACK(获取 Channel参数)的示例:

public class SecondConsumer {

    @RabbitHandler
    public void process(String msg){
        System.out.println(" second queue received msg : " + msg);
        // requeue:是否重新入队列,true:是; false:直接丢弃,相当于告诉队列可以直接删除掉
        // 丢弃消息
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
}

如果 requeue 参数设置为 true,可以把这条消息重新存入队列,以便发给下一个消费者(当然,只有一个消费者的时候,这种方式可能会出现无限循环重复消费的情况。可以投递到新的队列中,或者只打印异常日志)

五、消费者回调

  • 调用生产者 API
  • 发送响应消息给生产者

六、补偿机制

消费者处理时间太长或者网络超时,可能会导致生产者的 API 没有被调用,也没有收到消费者的响应消息。

生产者与消费者之间应该约定一个超时时间,对于超出时间没有得到响应的消息,才确定为消费世纪百,比如 5 分钟。5分钟,对于临时性故障的处理,比如网络恢复,或者重启应用,重启数据库,应该够了。

过了5分钟依然没有得到回复的消息,我们才判断为消费失败。这个时候我们要重发。

可以使用轮训本地消息表对没有发送到 MQ 的消息进行重发。重发时,在消费者需要进行幂等设计处理,防止脏数据。对于本地消息表,可以抽离出来做成消息服务系统,毕竟不只是一个服务在使用,抽离出来可以作为共用。

七、消息的幂等性

怎样处理相同消息的重复处理问题,首先我们要知道消息出现重复可能的原因:

  • 生产者的问题,环节①重复发送消息,比如在开启了 Confirm 模式但未收到确认,消费者重复投递。
  • 环节④出了问题,由于消费者未发送 ACK 或者其他原因,消息重复消费。
  • 生产者代码或者网络问题。

对于重复发送的消息,可以对每一条消息生成一个唯一的业务ID,通过日志或者消息落库来做重复控制。

例如:在金融系统中有一个叫流水号的东西。不管你在柜面汇款,还是ATM取款,或者信用卡消费,都会有一个唯一的序号。通过这个序号就可以找到唯一的一笔消息。

参考:银行的重账控制环节,对于进来的每一笔交易,第一件要做的事情就是查询是否重复。

八、消息的顺序性

消息的顺序性指的是消费者消费消息的顺序跟生产者生产消息的顺序是一致的。

例如:商户信息同步到其他系统,有三个业务操作:1、新增门店2、绑定产品3、激活门店,这种情况下消息消费顺序不能颠倒(门店不存在时无法绑定产品和激活)。

在 RabbitMQ 中,一个队列有多个消费者时,由于不同的消费者消费消息的速度是不一样的,顺序无法保证。只有一个队列仅有一个消费者的情况才能保证顺序消费(不同的业务消息发送到不同的专用的队列)。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值