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 自己丢了数据
处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。
设置持久化,通常有两个步骤,而且要同时开启这两个才可以:
- 将 queue 的持久化标识 durable 设置为 true,代表是一个持久的队列;
- 发送消息的时候将 deliveryMode = 2,这样消息就会被设为持久化方式,此时 RabbitMQ 就会将消息持久化到磁盘上;
这样设置以后,RabbitMQ 就算挂了,重启后也能恢复数据。
而且,这个持久化配置可以和 confirm 机制配合使用,在消息持久化磁盘后,再给生产者发送一个 ack 信号。这样,如果消息持久化磁盘之前,RabbitMQ 阵亡了,那么生产者收不到 ack 信号,生产者会自动重发。
而且持久化可以跟生产的 confirm 机制配合起来,只有消息持久化到了磁盘之后,才会通知生产者ack,这样就算是在持久化之前rabbitmq挂了,数据丢了,生产者收不到ack回调也会进行消息重发。
再补充说明一种情况,如果在消息还没有持久化到硬盘时,可能服务已经死掉?这种情况可以通过引入 mirrored-queue即:镜像队列,但也不能保证消息百分百不丢失(整个集群都挂掉)。
3.消费端弄丢了数据
启用手动确认模式可以解决这个问题。
- 关闭 autoAck 功能,启用手动确认模式;
- 每次在确保处理完这个消息之后,在代码里手动调用 ack,以确保消息一定被消费;
如果打开的的是 autoAck 模式,当消费者处理了数据以后,消费者会自动通知 RabbitMQ 数据已经被消费。如果此时不巧,消费者还没有处理完服务器就宕机了,而 RabbitMQ 和 消费者都以为消息已经被消费,不会触发重发机制,这就会造成消息丢失。
介绍下,消费者端移动有3种模式可供选择:
- 自动确认模式:消费者挂掉,待 ack 的消息回归到队列中。消费者抛出异常,消息会不断的被重发,直到处理成功。不会丢失消息,即便服务挂掉,没有处理完成的消息会重回队列,但是异常会让消息不断重试。
- 手动确认模式:如果消费者来不及处理就死掉时,没有响应 ack 时会重复发送一条信息给其他消费者;如果监听程序处理异常了,且未对异常进行捕获,会一直重复接收消息,然后一直抛异常;如果对异常进行了捕获,但是没有在 finally 里ack,也会一直重复发送消息(重试机制)。
- 不确认模式:acknowledge="none" 不使用确认机制,只要消息发送完成会立即在队列移除,无论客户端异常还是断开,只要发送完就移除,不会重发。
问题3:如何保证消费的顺序性?
如果是1个生产者,多个消费者的情况。生成的顺序是数据1、数据2、数据3,而消费的数据是分发的,不能确保都发到一个消费者手里,更不能确保一定是按照 1 - 2 - 3 的顺序被消费掉。
针对这个问题,可以通过某种算法,我的思路:是将需要保持先后顺序的消息放到同一个消息队列中。然后只用一个消费者去消费该队列。
同一个 Queue 里的消息一定是有序的,同一个消费者从 Queue 中消费也一定是有序的。
分享一篇文章,说的更详细一点:消息队列如何确保消息的有序性? - 知乎要想实现消息有序,需要从 Producer 和 Consumer 两方面来考虑。 首先,Producer 生产消息的时候就必须要有序。 然后,Consumer 消费的时候,也要按顺序来,不能乱。 Producer 有序 像 RabbitMQ 这类普通的消息系…https://zhuanlan.zhihu.com/p/372469047希望可以帮到你。