RabbitMQ数据一致性和消息积压问题

RabbitMQ数据一致性和消息积压问题

案例背景

以淘宝为例,我们在购买商品时,通常会选择用淘金币去抵扣一部分的金额,在这个过程中,交易服务和淘金币服务通过 MQ 消息队列进行通信。在下单时,交易服务发送“扣减账户100个淘金币”的消息给 MQ 消息队列,而淘金币服务则在消费端消费这条命令,实现真正的扣减操作。

image-20250517104256930

案例分析

引入 MQ 消息中间件最直接的目的是:系统解耦和流量控制。

  • 系统解耦:使用 MQ 消息队列,可以隔离系统上下游环境变化所带来的不稳定因素。比如,淘金币服务的系统需求无论如何变化,交易服务不用做任何改变。即时淘金币服务出现故障,主交易流程也可以将淘金币服务降级,实现交易服务和淘金币服务的解耦,实现了系统的高可用。
  • 流量控制:如果遇到秒杀等流量突增的场景,通过 MQ 还可以实现流量的“削峰填谷”的作用,可以根据下游的处理能力自动调节流量。

引入 MQ 消息中间件实现系统解耦,会影响系统之间数据传输的一致性。 在分布式系统中,如果两个节点之间存在数据同步,就会带来数据一致性的问题。 -> 消息生产端和消息消费端的消息数据一致性问题(也就是如何确保消息不丢失)。

引入 MQ 消息中间件解决流量控制,会使消费端处理能力不足从而导致消息积压。

image-20250517110348408

消息丢失处理逻辑

思维逻辑:哪些环节可能丢失消息?-> 如何知道消息是否丢失了?->如何确保消息不丢失?

首先,思考哪些环节可能丢失消息?一条消息从生产到消费完成这个过程,可以划分三个阶段,分别为消息生产阶段,消息存储阶段和消息消费阶段。

image-20250517111834122

消息生产阶段:从消息被生产出来,然后提交给 MQ 的过程中,只要能正常收到 MQ Broker 的 ack 确认响应,就表示发送成功,所以只要处理好返回值和异常,这个阶段是不会出现消息丢失的。

消息存储阶段:这个阶段一般会直接交给 MQ 消息中间件来保证,比如 Broker 会做副本,保证一条消息至少同步两个节点再返回 ack。

消息消费阶段:消费端从 Broker 上拉取消息,只要消费端在收到消息后,不立即发送消费确认给 Broker,而是等到执行完业务逻辑后,再发送消费确认,也能保证消息的不丢失。

上述方案看似万无一失,每个阶段都能保证消息的不丢失,但在分布式系统中,故障不可避免(比如,硬件故障、进程崩溃、网络因素),作为消息生产端,你并不能保证 MQ 是不是弄丢了你的消息,消费者是否消费了你的消息。所以为了系统的容错性,还需要设计一种消息检测机制,来检测消息是否丢失了。

消息检测机制->消息是否丢失

总体思路:在消息生产端,给每个发出的消息都指定一个全局唯一 ID,或者附加一个连续递增的版本号,然后在消费端做对应的版本校验。(每条消息携带一个版本号(如 1, 2, 3…),如果系统看到版本号从 1 跳到了 3,就能判断第 2 条消息可能丢失了。)

具体实现:利用拦截器机制。在生产端发送消息之前,通过拦截器将消息版本号注入消息中(版本号可以采用连续递增的 ID 生成)。然后在消费端收到消息后,在通过拦截器检测版本号的连续性或消费状态。(好处:消息检测的代码不会侵入到业务代码中,可以通过单独的任务来定位丢失的消息,做进一步的排查。)

[!note]

如果同时存在多个消息生产端和消息消费端,通过版本号递增的方式很难判断消息是否丢失,此时只能通过全局唯一 ID 的方案来进行消息检测。 why?-> 因为不同生产者各自生成版本号,可能导致多个消息有相同的版本号。并且没有一个全局的顺序,不能反映整个系统中消息的先后关系。如下所示,P1 和 P2 都用了版本号 v2v3。如果系统收到消息 [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,就可以明确知道是哪条消息丢失了。

现在,我们知道了消息存储阶段、消息消费阶段可能会出问题,并有了如何检测消息丢失的方案,接下来给出防止消息丢失的方案:如果检测到消息丢失,通过补偿机制,让发送方执行重试

消息被重复消费(幂等性问题)

比如:在消息消费的过程中,如果出现失败的情况,通过补偿的机制发送方会执行重试,重试的过程就有可能产生重复的消息,那么如何解决这个问题?

这个问题其实可以换一种说法,就是如何解决消费端幂等性问题(幂等性,就是一条命令,任意多次执行所产生的影响均与一次执行的影响相同),只要消费端具备了幂等性,那么重复消费消息的问题也就解决了。

如何让消费端具备幂等性? -> 通过扣减淘宝淘金币的例子解释:

image-20250517181317475

最简单的实现方案,就是在数据库中建一张消息日志表,这个表有两个字段:消息 ID 和消息执行状态。这样,我们消费消息的逻辑可以变为:在消息日志表中增加一条消息记录,然后再根据消息记录,去更新用户淘金币余额。因为我们每次都会在插入消息之前检查是否消息已存在,所以就不会出现一条消息被执行多次的情况,这样就实现了一个幂等的操作。

[!note]

想要解决“消息丢失”和“消息重复消费”的问题,有一个前提条件就是要实现一个全局唯一 ID 生成的技术方案。

全局唯一ID生成

image-20250517193000221

选择方案的时候,要站在实际的业务中,说明你的选型所考虑的平衡点是什么?

我个人在业务中比较倾向于选择 Snowflake 算法,在项目中也进行了一定的改造,主要是让算法中的 ID 生成规则更加符合业务特点,并且优化时钟回拨问题。

消息积压问题

如果出现积压,那一定是性能问题,想要解决消息从生产到消费上的性能问题,首先要知道哪些环节可能出现消息积压?然后在考虑如何解决? =>

哪些环节:因为消息发送之后才会出现积压的问题,所以和消息生产端没有关系,又因为绝大部分的消息队列单节点都能达到每秒钟几万的处理能力,相对于业务逻辑来说,性能不会出现在中间件的消息存储上面。那么出问题的肯定是消息消费阶段。 =>

怎么解决

  • 如果是出现了突发问题。那么要临时扩容,增加消费端的数量,与此同时,降级一些非核心的业务。通过扩容和降级承担流量。
  • 其次,才是排查解决异常问题。比如通过监控,日志等手段,分析是否消费端的业务逻辑代码出现了问题,优化消费端的业务处理逻辑。
  • 最后,如果是消费端的处理能力不足,可以通过水平扩容来提供消费端的并发处理能力。

🤗🤗🤗

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值