RabbitMQ数据一致性和消息积压问题
案例背景
以淘宝为例,我们在购买商品时,通常会选择用淘金币去抵扣一部分的金额,在这个过程中,交易服务和淘金币服务通过 MQ 消息队列进行通信。在下单时,交易服务发送“扣减账户100个淘金币”的消息给 MQ 消息队列,而淘金币服务则在消费端消费这条命令,实现真正的扣减操作。
案例分析
引入 MQ 消息中间件最直接的目的是:系统解耦和流量控制。
- 系统解耦:使用 MQ 消息队列,可以隔离系统上下游环境变化所带来的不稳定因素。比如,淘金币服务的系统需求无论如何变化,交易服务不用做任何改变。即时淘金币服务出现故障,主交易流程也可以将淘金币服务降级,实现交易服务和淘金币服务的解耦,实现了系统的高可用。
- 流量控制:如果遇到秒杀等流量突增的场景,通过 MQ 还可以实现流量的“削峰填谷”的作用,可以根据下游的处理能力自动调节流量。
引入 MQ 消息中间件实现系统解耦,会影响系统之间数据传输的一致性。 在分布式系统中,如果两个节点之间存在数据同步,就会带来数据一致性的问题。 -> 消息生产端和消息消费端的消息数据一致性问题(也就是如何确保消息不丢失)。
引入 MQ 消息中间件解决流量控制,会使消费端处理能力不足从而导致消息积压。
消息丢失处理逻辑
思维逻辑:哪些环节可能丢失消息?-> 如何知道消息是否丢失了?->如何确保消息不丢失?
首先,思考哪些环节可能丢失消息?一条消息从生产到消费完成这个过程,可以划分三个阶段,分别为消息生产阶段,消息存储阶段和消息消费阶段。
消息生产阶段:从消息被生产出来,然后提交给 MQ 的过程中,只要能正常收到 MQ Broker 的 ack 确认响应,就表示发送成功,所以只要处理好返回值和异常,这个阶段是不会出现消息丢失的。
消息存储阶段:这个阶段一般会直接交给 MQ 消息中间件来保证,比如 Broker 会做副本,保证一条消息至少同步两个节点再返回 ack。
消息消费阶段:消费端从 Broker 上拉取消息,只要消费端在收到消息后,不立即发送消费确认给 Broker,而是等到执行完业务逻辑后,再发送消费确认,也能保证消息的不丢失。
上述方案看似万无一失,每个阶段都能保证消息的不丢失,但在分布式系统中,故障不可避免(比如,硬件故障、进程崩溃、网络因素),作为消息生产端,你并不能保证 MQ 是不是弄丢了你的消息,消费者是否消费了你的消息。所以为了系统的容错性,还需要设计一种消息检测机制,来检测消息是否丢失了。
消息检测机制->消息是否丢失
总体思路:在消息生产端,给每个发出的消息都指定一个全局唯一 ID,或者附加一个连续递增的版本号,然后在消费端做对应的版本校验。(每条消息携带一个版本号(如 1, 2, 3…),如果系统看到版本号从 1 跳到了 3,就能判断第 2 条消息可能丢失了。)
具体实现:利用拦截器机制。在生产端发送消息之前,通过拦截器将消息版本号注入消息中(版本号可以采用连续递增的 ID 生成)。然后在消费端收到消息后,在通过拦截器检测版本号的连续性或消费状态。(好处:消息检测的代码不会侵入到业务代码中,可以通过单独的任务来定位丢失的消息,做进一步的排查。)
[!note]
如果同时存在多个消息生产端和消息消费端,通过版本号递增的方式很难判断消息是否丢失,此时只能通过全局唯一 ID 的方案来进行消息检测。 why?-> 因为不同生产者各自生成版本号,可能导致多个消息有相同的版本号。并且没有一个全局的顺序,不能反映整个系统中消息的先后关系。如下所示,P1 和 P2 都用了版本号
v2
和v3
。如果系统收到消息[v1, v2, v3]
,无法判断是否来自 P1 还是 P2,也无法判断是否有消息丢失。
生产者 P1: [v1]——>[v2]————>[v3]
│
生产者 P2: └────>[v2]——>[v3]
使用全局唯一 ID,每条消息都有唯一 ID,不管谁发的:
生产者 P1: [ID:1001]——>[ID:1002]————>[ID:1003]
生产者 P2: └────>[ID:2001]——>[ID:2002]
系统预期收到的消息可能是:1001, 1002, 1003, 2001, 2002。此时,如果少了 2001,就可以明确知道是哪条消息丢失了。
现在,我们知道了消息存储阶段、消息消费阶段可能会出问题,并有了如何检测消息丢失的方案,接下来给出防止消息丢失的方案:如果检测到消息丢失,通过补偿机制,让发送方执行重试。
消息被重复消费(幂等性问题)
比如:在消息消费的过程中,如果出现失败的情况,通过补偿的机制发送方会执行重试,重试的过程就有可能产生重复的消息,那么如何解决这个问题?
这个问题其实可以换一种说法,就是如何解决消费端幂等性问题(幂等性,就是一条命令,任意多次执行所产生的影响均与一次执行的影响相同),只要消费端具备了幂等性,那么重复消费消息的问题也就解决了。
如何让消费端具备幂等性? -> 通过扣减淘宝淘金币的例子解释:
最简单的实现方案,就是在数据库中建一张消息日志表,这个表有两个字段:消息 ID 和消息执行状态。这样,我们消费消息的逻辑可以变为:在消息日志表中增加一条消息记录,然后再根据消息记录,去更新用户淘金币余额。因为我们每次都会在插入消息之前检查是否消息已存在,所以就不会出现一条消息被执行多次的情况,这样就实现了一个幂等的操作。
[!note]
想要解决“消息丢失”和“消息重复消费”的问题,有一个前提条件就是要实现一个全局唯一 ID 生成的技术方案。
全局唯一ID生成
选择方案的时候,要站在实际的业务中,说明你的选型所考虑的平衡点是什么?
我个人在业务中比较倾向于选择 Snowflake 算法,在项目中也进行了一定的改造,主要是让算法中的 ID 生成规则更加符合业务特点,并且优化时钟回拨问题。
消息积压问题
如果出现积压,那一定是性能问题,想要解决消息从生产到消费上的性能问题,首先要知道哪些环节可能出现消息积压?然后在考虑如何解决? =>
哪些环节:因为消息发送之后才会出现积压的问题,所以和消息生产端没有关系,又因为绝大部分的消息队列单节点都能达到每秒钟几万的处理能力,相对于业务逻辑来说,性能不会出现在中间件的消息存储上面。那么出问题的肯定是消息消费阶段。 =>
怎么解决:
- 如果是出现了突发问题。那么要临时扩容,增加消费端的数量,与此同时,降级一些非核心的业务。通过扩容和降级承担流量。
- 其次,才是排查解决异常问题。比如通过监控,日志等手段,分析是否消费端的业务逻辑代码出现了问题,优化消费端的业务处理逻辑。
- 最后,如果是消费端的处理能力不足,可以通过水平扩容来提供消费端的并发处理能力。
🤗🤗🤗