RabbitMQ 如何确保消息的成功投递?幂等性?顺序性?

RabbitMQ 如何确保消息的成功投递?RabbitMQ 如何保证不重复消费,保证数据不丢失?分布式系统里,如何保证数据的一致性?一串连环炮你是否顶得住?

其实这几个问题的原理大同小异,都可以在统一的思路上解答。

先明白一点,问什么我们的业务需要用到MQ,不管是RabitMQ,kafka,还是RocketMQ?

说白了,MQ异步处理是互联网分层架构中的解耦利器,可以帮助系统处理高并发,削峰填谷。

  • 解耦:使服务相对独立,降低强依赖性,减少代码的维护成本,增强可扩展性;
  • 异步:将消息写入消息队列,非必要的业务逻辑以异步的方式运行,加快响应速度;
  • 削峰:并发量大的时候,避免所有的请求直接压到数据库;

我们拿 RabitMQ(其它同理) 举例,展示一条消息从生产到消费的过程:

明白了这个过程,我们再一起看问题。


问题1:如何确保不重复消费?

保证消息不被重复消费的关键,是保证消息队列的幂等性,针对这个问题,一般使用 Redis 全局ID的方法解决。

  • 每一个消息在发送的时候,都分配一个全局唯一的ID作为标识;
  • 消费者开始消费前,先去 Redis 中查询有没消费记录;
  • 没有记录则处理数据,成功以后,将ID保存;
  • 有记录则不消费,保证每条记录只消费一次;

问题2:如何确保消息的成功投递?

这个就是MQ使用过程中最典型的问题,依据上面的图,我们来逐一分析数据丢失的场景:

  • 生产者弄丢了数据
  • RabbitMQ 自己丢了数据
  • 消费端弄丢了数据

1.生产者弄丢了数据

生产者将数据发送到 RabbitMQ 的时候,可能在传输过程中因为网络等问题而将数据弄丢了。针对这种情况,有2种办法解决,先介绍再说结论。

  • RabbitMQ 的事务功能

        生产者在发送数据之前开启事物,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会受到异常报错,这时就可以回滚事物,然后尝试重新发送;如果收到了消息,那么就可以提交事物。

// 开启事物
channel.txSelect();
try {
    // 发送消息
} catch(Exection e) {
    // 回滚事物
    channel.txRollback();
    // 重新提交
}

但是,这种方法有个致命的缺点:事物开启,MQ 就会变为同步阻塞操作,生产者会阻塞等待是否发送成功,有悖 MQ 的异步初衷不说,还会降低系统的吞吐量。

  • 开启 confirm 模式

        RabbitMQ 提供可靠性消息投递模式(confirm)。

        生产者设置开启了 confirm 模式之后,每次写的消息都会分配一个唯一的 ID,然后如何写入了 RabbitMQ 之中,RabbitMQ 会给你回传一个 ack 消息,告诉你这个消息发送 OK 了;如果RabbitMQ 没能处理这个消息,会回调你一个 nack 接口,告诉你这个消息失败了,你可以进行重试。

        而且你可以结合这个机制知道自己在内存里维护每个消息的 ID,如果超过一定时间还没接收到这个消息的回调,那么你可以进行手动重发。比如:像保证消息的幂等性一样,在 Redis 中存入消息的唯一性ID,只有在成功接收到 ack 消息以后才会删除,否则会定时重发。

    // 开启confirm
    channel.confirm();
    // 发送成功回调
    public void ack(String messageId){

    }
    // 发送失败回调
    public void nack(String messageId){
        // 重发该消息
    }

上面的2方法中,通常都是用 confirm 机制来避免消费者的消息丢失。

2.RabbitMQ 自己丢了数据

处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。

设置持久化,通常有两个步骤,而且要同时开启这两个才可以:

  1. 将 queue 的持久化标识 durable 设置为 true,代表是一个持久的队列;
  2. 发送消息的时候将 deliveryMode = 2,这样消息就会被设为持久化方式,此时 RabbitMQ  就会将消息持久化到磁盘上;

这样设置以后,RabbitMQ 就算挂了,重启后也能恢复数据。

而且,这个持久化配置可以和 confirm 机制配合使用,在消息持久化磁盘后,再给生产者发送一个 ack 信号。这样,如果消息持久化磁盘之前,RabbitMQ 阵亡了,那么生产者收不到 ack 信号,生产者会自动重发。

而且持久化可以跟生产的 confirm 机制配合起来,只有消息持久化到了磁盘之后,才会通知生产者ack,这样就算是在持久化之前rabbitmq挂了,数据丢了,生产者收不到ack回调也会进行消息重发。

再补充说明一种情况,如果在消息还没有持久化到硬盘时,可能服务已经死掉?这种情况可以通过引入 mirrored-queue即:镜像队列,但也不能保证消息百分百不丢失(整个集群都挂掉)。

3.消费端弄丢了数据

启用手动确认模式可以解决这个问题。

  1. 关闭 autoAck 功能,启用手动确认模式;
  2. 每次在确保处理完这个消息之后,在代码里手动调用 ack,以确保消息一定被消费;

如果打开的的是 autoAck 模式,当消费者处理了数据以后,消费者会自动通知 RabbitMQ 数据已经被消费。如果此时不巧,消费者还没有处理完服务器就宕机了,而 RabbitMQ 和 消费者都以为消息已经被消费,不会触发重发机制,这就会造成消息丢失。

介绍下,消费者端移动有3种模式可供选择:

  1. 自动确认模式:消费者挂掉,待 ack 的消息回归到队列中。消费者抛出异常,消息会不断的被重发,直到处理成功。不会丢失消息,即便服务挂掉,没有处理完成的消息会重回队列,但是异常会让消息不断重试。
  2. 手动确认模式:如果消费者来不及处理就死掉时,没有响应 ack 时会重复发送一条信息给其他消费者;如果监听程序处理异常了,且未对异常进行捕获,会一直重复接收消息,然后一直抛异常;如果对异常进行了捕获,但是没有在 finally 里ack,也会一直重复发送消息(重试机制)。
  3. 不确认模式:acknowledge="none" 不使用确认机制,只要消息发送完成会立即在队列移除,无论客户端异常还是断开,只要发送完就移除,不会重发。

问题3:如何保证消费的顺序性?

如果是1个生产者,多个消费者的情况。生成的顺序是数据1、数据2、数据3,而消费的数据是分发的,不能确保都发到一个消费者手里,更不能确保一定是按照 1 - 2 - 3 的顺序被消费掉。

针对这个问题,可以通过某种算法,我的思路:是将需要保持先后顺序的消息放到同一个消息队列中。然后只用一个消费者去消费该队列。

同一个 Queue 里的消息一定是有序的,同一个消费者从 Queue 中消费也一定是有序的。

分享一篇文章,说的更详细一点:消息队列如何确保消息的有序性? - 知乎要想实现消息有序,需要从 Producer 和 Consumer 两方面来考虑。 首先,Producer 生产消息的时候就必须要有序。 然后,Consumer 消费的时候,也要按顺序来,不能乱。 Producer 有序 像 RabbitMQ 这类普通的消息系…https://zhuanlan.zhihu.com/p/372469047希望可以帮到你。


  • 9
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java Punk

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值