RabbitMQ:怎么保证消息100%被消费

我们知道,消息从生产端到消费端消费要经过 3 个步骤:

  • 生产端发送消息到 RabbitMQ
  • RabbitMQ 发送消息到消费端
  • 消费端消费这条消息

image.png

这 3 个步骤中的每一步都有可能导致消息丢失,消息丢失不可怕,可怕的是丢失了我们还不知道,所以要有一些措施来保证系统的可靠性。这里的可靠并不是一定就 100% 不丢失了,磁盘损坏,机房着火等等都能导致数据丢失,当然这种都是极小概率发生,能做到99.99999% 消息不丢失,就是可靠的了。

下面来具体分析一下问题以及解决方案。

生产端可靠性投递

生产端可靠性投递,即生产端要确保消息正确投递到 RabbitMQ 中。生产端投递的消息丢失的原因有很多,比如消息在网络传输的过程中发生网络故障,消息丢失;又或者消息投递到 RabbitMQ 时,RabbitMQ 挂了,那消息也可能丢失,而我们根本不知道发生了什么。针对以上情况,RabbitMQ 本身提供了一些机制。

事务消息机制

RabbitMQ 中与事务机制有关的方法有三个:txSelect()txCommit()txRollback(), txSelect用于将当前 channel 设置成 transaction 模式,txCommit 用于提交事务,txRollback 用于回滚事务,在通过 txSelect 开启事务之后,我们便可以发布消息给 broker 代理服务器了,如果 txCommit 提交成功了,则消息一定到达了 broker 了,如果在 txCommit 执行之前 broker 异常崩溃或者由于其他原因抛出异常,这个时候我们便可以捕获异常通过 txRollback 回滚事务了。

try {
    channel.txSelect()
    channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, MessageProperties.PERSISTENT_TEXT_PLAIN,  msg.toByteArray(StandardCharsets.UTF_8))
    channel.txCommit()
}catch (e: Exception){
    channel.txRollback()
    // 进行消息重发
    channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, MessageProperties.PERSISTENT_TEXT_PLAIN,  msg.toByteArray(StandardCharsets.UTF_8))
}
复制代码

事务确实能够解决 producer 与 broker 之间消息确认的问题,只有消息成功被 broker 接受,事务提交才能成功,否则我们便可以在捕获异常进行事务回滚操作同时进行消息重发。

事务消息机制由于会严重降低性能,所以一般不采用这种方法。那么有没有更好的方法既能保障producer知道消息已经正确送到,又能基本上不带来性能上的损失呢?从AMQP协议的层面看是没有更好的方法,但是RabbitMQ提供了一个更好的方案,即将channel信道设置成confirm模式。

confirm 消息确认机制

什么是confirm消息确认机制?顾名思义,就是生产端投递的消息一旦投递到RabbitMQ后,RabbitMQ就会发送一个确认消息给生产端,让生产端知道我已经收到消息了,否则这条消息就可能已经丢失了,需要生产端重新发送消息了。

image.png

通过下面这句代码来开启确认模式:

channel.confirmSelect() // 开启生产者确认模式
复制代码

然后异步监听确认和未确认的消息:

channel.addConfirmListener(object: ConfirmListener{
    override fun handleAck(deliveryTag: Long, multiple: Boolean) {
        // 已收到消息,做一些处理
    }

    override fun handleNack(deliveryTag: Long, multiple: Boolean) {
        // RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack消息
        // 做一些其他处理,比如消息重发等
    }
})
复制代码

这样就可以让生产端感知到消息是否投递到RabbitMQ中了,当然这样还不够,稍后我会说一下极端情况。

消息持久化

那消息持久化呢?我们知道,RabbitMQ 收到消息后将这个消息暂时存在了内存中,那这就会有个问题,如果RabbitMQ 挂了,那重启后数据就丢失了,所以相关的数据应该持久化到硬盘中,这样就算 RabbitMQ 重启后也可以到硬盘中取数据恢复。那如何持久化呢?

message 消息到达 RabbitMQ 后先是到 exchange 交换机中,然后路由给 queue 队列,最后发送给消费端。

image.png

所以需要给 exchange、queue 和 message 都进行持久化。

exchange持久化:

// 第三个参数 true 代表这个 exchange 持久化
channel.exchangeDeclare(EXCHANGE_NAME, EXCHANGE_TYPE, true)
复制代码

queue持久化:

// 第二个参数 true 表示这个 queue 持久化
channel.queueDeclare(QUEUE_NAME, true, false, false, null)
复制代码

message持久化:

// 第三个参数MessageProperties.PERSISTENT_TEXT_PLAIN表示这条消息持久化
channel.basicPublish(EXCHANGE_NAME, ROUTING_KEY, MessageProperties.PERSISTENT_TEXT_PLAIN,  msg.toByteArray(StandardCharsets.UTF_8))
复制代码

这样,如果RabbitMQ收到消息后挂了,重启后会自行恢复消息。

到此,RabbitMQ提供的几种机制都介绍完了,但这样还不足以保证消息可靠性投递RabbitMQ中,上面我也提到了会有极端情况,比如RabbitMQ收到消息还没来得及将消息持久化到硬盘时,RabbitMQ挂了,这样消息还是丢失了。

image.png

所以除了RabbitMQ提供的一些机制外,我们自己也要做一些消息补偿机制,以应对一些极端情况。接下来我就介绍其中的一种解决方案——消息入库。

消息入库

消息入库,顾名思义就是将要发送的消息保存到数据库中。

首先发送消息前先将消息保存到数据库中,有一个状态字段 status=0,表示生产端将消息发送给了 RabbitMQ 但还没收到确认;在生产端收到确认后将 status 设为 1,表示 RabbitMQ 已收到消息。这里有可能会出现上面说的两种情况,所以生产端这边开一个定时器,定时检索消息表,将 status=0 并且超过固定时间后(可能消息刚发出去还没来得及确认这边定时器刚好检索到这条 status=0 的消息,所以给个时间)还没收到确认的消息取出重发(第二种情况下这里会造成消息重复,消费者端要做幂等性),可能重发还会失败,所以可以做一个最大重发次数,超过就做另外的处理。

image.png

这样消息就可以可靠性投递到RabbitMQ中了,而生产端也可以感知到了。

消费端不丢失

既然已经可以让生产端100%可靠性投递到RabbitMQ了,那接下来就改看看消费端的了,如何让消费端不丢失消息。

默认情况下,以下3种情况会导致消息丢失:

  • 在RabbitMQ将消息发出后,消费端还没接收到消息之前,发生网络故障,消费端与RabbitMQ断开连接,此时消息会丢失;
  • 在RabbitMQ将消息发出后,消费端还没接收到消息之前,消费端挂了,此时消息会丢失;
  • 消费端正确接收到消息,但在处理消息的过程中发生异常或宕机了,消息也会丢失。

image.png

其实,上述3中情况导致消息丢失归根结底是因为RabbitMQ的自动ack机制,即默认RabbitMQ在消息发出后就立即将这条消息删除,而不管消费端是否接收到,是否处理完,导致消费端消息丢失时RabbitMQ自己又没有这条消息了。

image.png

所以就需要将自动ack机制改为手动ack机制。

消费端手动确认消息:

channel.basicConsume(
    QUEUE_NAME,
    false,
    DeliverCallback { consumerTag, delivery ->
        try {
            //接收到消息,做处理
            //手动确认
            channel.basicAck(delivery.envelope.deliveryTag, false)
        } catch (e: java.lang.Exception) {
            //出错处理,这里可以让消息重回队列重新发送或直接丢弃消息
        }
    },
    CancelCallback {}
)
复制代码

这样,当 autoAck 参数置为 false,对于 RabbitMQ 服务端而言,队列中的消息分成了两个部分:一部分是等待投递给消费端的消息;一部分是已经投递给消费端,但是还没有收到消费端确认信号的消息。如果 RabbitMQ 一直没有收到消费端的确认信号,并且消费此消息的消费端已经断开连接或宕机( RabbitMQ 会自己感知到),则RabbitMQ 会安排该消息重新进入队列(放在队列头部),等待投递给下一个消费者,当然也有可能还是原来的那个消费端,当然消费端也需要确保幂等性。

好了,到此从生产端到RabbitMQ再到消费端的全链路,就可以保证数据的不丢失。

猜你喜欢

转载自juejin.im/post/7083465447642759175