您好, 欢迎来到 !    登录 | 注册 | | 设为首页 | 收藏本站

MQ问题总结

wiki 2022/2/27 11:55:52 架构 字数 212505 阅读 557

MQ问题总结

一、为什么使用 MQ?

1.1 解耦

1.1.1 解耦1

例如电商系统核心是交易服务,交易服务要调用另外三个服务,订单服务、库存服务、仓储服务。

这三个服务如果有一个服务不可用,交易服务就无法正常运行,所以交易服务是强耦合另外三个服务。

引入MQ之后,交易服务只跟MQ交互,把消息发到MQ里面就行了,无需关心另外三个服务是否可用。这时候交易服务跟另外三个服务就是弱耦合的关系,耦合性被降低了。

哪怕是另外三个服务暂时不可用,也不影响交易服务的运行,只要其他服务运行起来后,把MQ里面的消息消费了就行。

1.1.2 解耦2

假设 A 系统在用户发生某个操作的时候,需要把用户提交的数据同时推送到 B、C 两个系统的时候。这个时候负责 A 系统的哥们想:没事啊,B、C 两个系统给我提供一个 HTTP 接口或者 RPC 接口,我把数据推送过去不就完事了嘛。负责 A 系统的哥们美滋滋。

一切看起来很美好,但是随着业务快速迭代,这个时候系统 D 也想要这个数据。那既然这样,A 系统的开发同学就改咯,在发送数据给 BC 的同时加上一个 D。但是,越到后面越发现,麻烦来了。整个系统好像不止这个数据要发送给 BCD、还有第二、第三个数据要发送给 BCD。甚至有时候又加入了 E、F 等系统,他们也要这个数据。并且有时候可能 B 系统突然又不要这个数据了,A 系统改来改去,A 系统的开发哥们头皮发麻。更复杂的场景是,数据通过接口传给其他系统有时候还要考虑重试、超时等一些异常情况。

这个时候,就该我们的 MQ 粉墨登场了!这种情况下使用 MQ 来解耦是再合适不过了,因为负责 A 系统的哥们只需要把消息扔到 MQ 就行了,其他系统按需来订阅消息就好了。就算某个系统不需要这个数据了,也不会需要 A 系统改动代码。

1.2 异步

没有引入MQ的时候,交易服务需要同步调用三个服务,如果调用一个服务需要耗时1秒,那么同步调用三个服务需要耗时3秒。在引入MQ之后,全都改成了异步调用,整个耗时不到1秒,大大提高了接口的性能。

1.3 削峰

如果一秒内同时来了5000笔交易,而订单服务每秒只能处理100笔交易,那么后面的4900笔交易失败。在引入MQ之后,交易服务可以把交易数据先发送到MQ中,而订单服务再慢慢从MQ拉取交易信息处理。从而避免突发流量压垮服务器。

1.3.1 削峰填谷

举个例子,比如我们的订单系统,在下单的时候就会往数据库写数据。但是数据库只能支撑每秒 1000 左右的并发写入,并发量再高就容易宕机。低峰期的时候并发也就 100 多个,但是在高峰期时候,并发量会突然激增到 5000 以上,这个时候数据库肯定死了。

但是使用了 MQ 之后,情况就变了,消息被 MQ 保存起来了,然后系统就可以按照自己的消费能力来消费,比如每秒 1000 个数据,这样慢慢写入数据库,这样就不会打死数据库了。

至于为什么叫做削峰填谷呢?如果没有用 MQ 的情况下,并发量高峰期的时候是有一个“顶峰”的,然后高峰期过后又是一个低并发的“谷”。但是使用了 MQ 之后,限制消费消息的速度为 1000QPS,但是这样一来,高峰期产生的数据势必会被积压在 MQ 中,高峰就被“削”掉了。但是因为消息积压,在高峰期过后的一段时间内,消费消息的速度还是会维持在 1000QPS,直到消费完积压的消息,这就叫做“填谷”。

二、引入MQ之后的问题

2.1 系统可用性降低

本来整个系统有四个服务,我们只需要保证这四个服务可用就行了。现在又多引入了一个MQ,我们还要保证MQ的可用,所以整个系统的可用性降低。

2.2 系统复杂性提高

本来交易服务是同步调用另外三个服务,如果另外三个服务不可用,交易服务能立即感知到。引入MQ之后,整个系统的稳定性就要靠MQ保证了。

这时候,我们就要考虑到发到MQ里面的消息怎么避免丢失的问题?顺序性消费的问题,就是同一笔交易的下单消息应该比撤单消息先处理。重复性消费的问题,就是同一笔下单交易的消息可能被多次处理。

当然,每种问题都有具体的解决方案,避免消息丢失可以使用MQ集群,顺序性消费可以把消息发到同一个分区,重复性消费可以在消费端做幂等性处理。

2.3 重复消费问题

2.3.1 问题场景

重复消费问题可以说是 MQ 中普遍存在的问题, 不管你用哪种 MQ 都无法避免。有哪些场景会出现重复的消息呢?

  • 消息生产者产生了重复的消息;

  • Kafka 和 RocketMQ 的 offset 被回调了;

  • 消息消费者确认失败;

  • 消息消费者确认时超时;

  • 业务系统主动发起重试。

如果重复消息不做正确的处理,会对业务造成很大的影响,产生重复数据或者导致数据异常,比如会员系统多开通了一个月的会员等。

2.3.2 解决方案

不管是由于生产者产生的重复消息,还是由于消费者导致的重复消息,我们都可以在消费者中解决这个问题。

这就要求消费者在做业务处理时,要做幂等设计。在这里我推荐增加一张消费消息表,来解决 MQ的这类问题。

消费消息表中,使用 messageId 做唯一索引。在处理业务逻辑之前,先根据 messageId 查询一下该消息有没有处理过。如果已经处理过了则直接返回成功,如果没有处理过,则继续做业务处理。

2.4 数据一致性问题

2.4.1 问题场景

当服务间是同步调用的时候,我们还可以使用本地事务来控制数据的一致性。但是引入MQ之后,服务间的调用都是异步了,就没办法使用本地事务,也就无法做到数据的强一致性了。

例如,调用订单服务下单成功了,但是调用库存服务扣减库存失败,就会导致超卖,是严重的线上事故。

这时候怎么办?可以使用MQ事务消息(只有RocketMQ才有事务消息功能),相当于是同步调用,会严重降低性能。

2.4.2 解决方案

我们都知道数据一致性分为:强一致性、弱一致性、最终一致性。

而 MQ 为了性能考虑使用的是最终一致性,那么必定会出现数据不一致的问题。这类问题大概率是因为消费者读取消息后,业务逻辑处理失败导致的。这时候可以增加重试机制。重试分为同步重试和异步重试。

有些消息量比较小的业务场景,可以采用同步重试。在消费消息时如果处理失败,立刻重试 3-5 次,如果还是失败则写入到记录表中。但如果消息量比较大,则不建议使用这种方式。因为如果出现网络异常,可能会导致大量的消息不断重试,影响消息读取速度造成消息堆积。


消息量比较大的业务场景,建议采用异步重试。在消费者处理失败之后,立刻写入重试表,有个 job(如采用xxljob) 专门定时重试。

还有一种做法:如果消费失败,自己给同一个 topic 发一条消息。在后面的某个时间点,自己又会消费到那条消息,起到了重试的效果。如果对消息顺序要求不高的场景,可以使用这种方式。

2.5 消息丢失问题

2.5.1 问题场景

同样消息丢失问题,也是 MQ 中普遍存在的问题,不管你用哪种 MQ 都 无法避免。有哪些 场景会出现消息丢失问题呢?

  • 生产者产生消息时,由于网络原因发送到 MQ 失败了;

  • MQ 服务器持久化,存储磁盘时出现异常;

  • Kafka和RocketMQ 的 offset 被回调时,略过了很多消息;

  • 消费者刚读取消息,已经 ACK 确认,但业务还没处理完,服务就被重启了。

导致消息丢失问题的原因挺多的, 生产者、 MQ 服务器、 消费者都有可能产生问题。我在这里就不一一列举了。最终的结果会导致消费者无法正确的处理消息,而导致数据不一致的情况。

2.5.2 解决方案

不管你是否承认,有时候消息真的会丢。即使这种概率非常小,也会对业务有影响。生产者、MQ 服务器、消费者都有可能会导致消息丢失的问题。为了解决这个问题,我们可以增加一张消息发送表。

  • 当生产者发完消息之后,会往该表中写入一条数据,状态 status 标记为待确认;

  • 如果消费者读取消息之后,调用生产者的 API 更新该消息的status为已确认;

  • 有个job(xxljob) 每隔一段时间检查一次消息发送表,如果5分钟(这个时间可以根据实际情况来定)后还有状态是待确认的消息,则认为该消息已经丢失了,重新发条消息。

这样不管是由于生产者、 MQ服务器、还是消费者导致的消息丢失问题,job 都会重新发消息。

2.6 消息顺序问题

2.6.1 问题场景

有些业务数据是有状态的,比如订单有下单、支付、完成、退货等状态。如果订单数据作为消息体,就会涉及顺序问题了。

例如消费者收到同一个订单的两条消息。第一条消息的状态是下单,第二条消息的状态是支付,这是没问题的。但如果第一条消息的状态是支付,第二条消息的状态是下单就会有问题了。没有下单就先支付了?


消息顺序问题是一个非常棘手的问题,比如:

  • Kafka 同一个 partition 中能保证顺序,但是不同的 partition 无法保证顺序;

  • RabbitMQ的同一个queue能够保证顺序,但是如果多个消费者同一个queue 也会有顺序问题。

  • 如果消费者使用多线程消费消息,也无法保证顺序。

  • 如果消费消息时同一个订单的多条消息中,中间的一条消息出现异常情况,顺序将会被打乱。

  • 还有如果生产者发送到 MQ中的路由规则,跟消费者不一样,也无法保证顺序。

2.6.2 解决方案

消息顺序问题是一种常见问题。我们以 Kafka 消费订单消息为例,订单有下单、 支付、 完成、 退货等状态。这些状态是有先后顺序的,如果顺序错了会导致业务异常。

解决这类问题之前,我们需要先确认:消费者是否真的需要知道中间状态,只知道最终状态行不行?


其实很多时候,我真的需要知道的是最终状态。这时可以把流程优化一下:

这种方式可以解决大部分的消息顺序问题。

但如果真的有需要保证消息顺序的需求,那么可以将订单号路由到不同的 partition。同一个订单号的消息,每次到发到同一个partition。

2.7 消息堆积

2.7.1 问题场景

如果消息消费者读取消息的速度,能够跟上消息生产者的节奏,那么整套 MQ 机制就能发挥最大作用。

但是很多时候,由于某些批处理或者其他原因,导致消费速度小于生产速度。这样会直接导致消息堆积问题,从而影响业务功能。

这里以下单 开通会员为例,如果消息出现堆积会导致用户下单之后,很久之后才能变成会员。这种情况肯定会引起大量用户投诉。

2.7.2 解决方案

那么消息堆积问题该如何解决呢?这个要看消息是否需要保证顺序。如果不需要保证顺序,可以读取消息之后用多线程处理业务逻辑。


这样就能增加业务逻辑处理速度,解决消息堆积问题。但是线程池的核心线程数和最大线程数需要合理配置,不然可能会浪费系统资源。

如果需要保证顺序,可以读取消息之后将消息按照一定的规则分发到多个队列中,然后在队列中用单线程处理。


如果您也喜欢它,动动您的小指点个赞吧

除非注明,文章均由 laddyq.com 整理发布,欢迎转载。

转载请注明:
链接:http://laddyq.com
来源:laddyq.com
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


联系我
置顶