1. 前言

上个小节中我们介绍了 RabbitMQ 中如何防止消息丢失,即保证消息发送的 At Least Once 性质,除此之外,如何防止消息被重复消费,即保证消息消费的 Exactly Once 性质,也是业务逻辑中需要考虑的问题。

2. 消息消费顺序

面试官提问:业务中使用了 RabbitMQ 消息队列,如何保证消息的顺序消费?

题目解析

保证消息的顺序消费是业务场景下经常面临的挑战,可能在面试中会涉及到一些实战场景,例如电商的下单逻辑,在用户下单之后,会发送创建订单和扣减库存的消息,我们需要保证扣减库存在创建订单之后执行。

在MQ层面支持消息的顺序消费是一件开销很大的操作,例如使用事务,所以除非特定场景,一般不在 RabbitMQ 消息传输底层支持顺序。在上层即应用层处理业务逻辑是常规操作,有两种通用解决方案:

(1)同步发送消息:将消息发送从异步模式切换为同步模式,例如先发送创建订单消息,当创建订单的下游消费者发送ACK确认成功消费后,再发送扣减库存的消息;
(2)消息实体增加冗余字段:例如增加 version(版本号)、 msg_id(消息id),保证在扣减库存时,对应 msg_id 的订单已经创建成功,实战中配合Redis等缓存协助判断。

3. 消息重复消费

面试官提问:RabbitMQ 如何保证消息不会被重复消费?

题目解析:

所有的消息队列都要保证同一条消息不会被重复消费,RabbitMQ 重复消费消息的可能场景主要有两种:

(1)生产者重复发送消息:生产者在往消息队列发送消息时,发生了网络抖动,生产者没有收到确认信号,但是实际上消息队列已经收到了消息,超过一定时间后生产者会重新发送消息,这时一条消息被发送了两次;
(2)消费者重复接受消息:消费者成功消费消息后,发生了网络抖动,消息队列没有收到确认信号,超过一段时间后会重新给消费者投递相同的消息,同一条消息即存在被消费两次的可能。

通用解决方案是在消息实体中添加全局唯一的id,例如 msg_id(消息ID),在业务逻辑层保证消息的幂等性,具体参考步骤:

(1)消费者在收到消息之后,根据 msg_id 从缓存/数据库中查询是否存在已有消息;
(2)如果不存在已有消息,那么消费之后,将 msg_id 对应的消息实体或者序列化对象写入缓存/数据库;
(3)如果存在已有消息,说明这条消息已被消费过,丢弃消息并且打一条告警日志。

并且可以根据重复消费的容忍程度以及性能要求选择使用缓存还是使用数据库,如果对判断的速度要求高,可以使用 Redis 作为缓存;如果对判断的稳定性和鲁棒性要求高,使用数据库存储消息实体,同时将 msg_id 作为数据库表的唯一键,插入重复记录一定会抛出异常,避免数据库因为并发问题产生脏数据,保证了消息消费的不可重复性。

4. 小结

本章节介绍了 RabbitMQ 中最常见的重复发送消息的实际场景,并且给出了添加全局唯一 ID 的通用性解决方案,候选人需要理解通过全局 ID 解决重复消息的核心逻辑,准备时间充裕的情况可以在本地环境编码实现上述流程。