MQ 系列的第一篇,主要内容是介绍在分布式项目中引入消息队列的原因、消息队列的优缺点以及在面试中遇到此问题应该如何回答。

MQ1封面.png

1. 为什么使用消息队列

MQ 在分布式系统中的使用极为常见,究其原因,还是大家常说的三大点——解耦、异步、削峰。下面我就结合实际场景尝试为大家解释一下这三个使用消息队列的原因。

1.1 解耦

首先来引入一个最常见的实际场景:注册场景。在 BBC 商城中,用户通过注册会成为商城的会员,然后会员中心会调用通知中心的能力来初始化会员等级。

这里需要注意的是用户和会员是两个业务域中的不同概念,用户是用户中心的主模型,会员是会员中心的主模型,这两个业务域是共同构成 BBC 商城的两部分。

在这个用户注册成为会员的场景下,如果不使用 MQ,系统间交互是通过接口同步调用的方式进行的,业务流程如下:

后来 PD 又加了一个需求:注册成为会员后除了初始化会员等级,还需要初始化会员积分账户,于是在不使用 MQ 的场景下又在会员中心依赖了积分中心,同时在创建会员方法中新增一段逻辑,通过同步调用积分中心接口来完成初始化积分账户的功能,流程图如下:

后来 PD 又加了几个需求,现在会员中心要对接营销中心,有一个注册有礼的活动要求在会员注册后给他发放一张新人优惠券。

难道还是在创建会员的逻辑中新增一段同步调用逻辑吗?如果是的话,将来又要新增其他逻辑呢?如发送欢迎注册短信、为会员打系统自动标签等等。如果每新加一个功能都要在创建会员的逻辑中新增代码进行同步调用的话,代码维护起来将十分困难——至少需要一个 Convert 方法来做参数转换以及一个同步接口的方法调用。

更何况,如果现在由于平台预算限制,需要取消营销中心的对接,不再发放新人优惠券呢?难道是通过频繁改动上游系统代码来实现吗?

这都只是功能上带来的问题,如果在用户注册时,前面几个步骤都是正常的,而到了发放注册有礼优惠券的时候由于优惠券库存不足导致发放失败,用户注册的结果也将失败,这就带来一个很诡异很不对劲的现象:发放新人优惠券失败导致用户注册业务异常!

这两者本身从业务上来说并没有倒置的因果关系,只有注册失败才会导致发放新人优惠券失败,而发放新人优惠券失败不能也不应该引发注册失败的异常。换言之,即便发放新人优惠券失败,运营人员也可以通过补发优惠券等操作进行补偿,而不是让用户无法注册。

所以,在不使用消息队列的情况下,系统间交互是通过接口同步调用的方式进行的,这将带来以下问题:

  • 代码可维护性变差,无论上游系统是接入还是接触下游系统的对接,都需要修改代码重新部署上线
  • 上游系统需要兼顾多个下游系统的数据结构、调用异常、是否重试、分布式事务等,并且代码全都耦合在上游系统,代码可读性变差,系统复杂度提高
  • 系统间耦合度很高,会出现非核心业务影响主业务正常运行的情况

如果使用消息队列,上游系统只需要保证消息成功发送到消息中间件:

  • 下游系统想要对接就去 MQ 订阅消息,想解除对接关系就取消订阅。拿上面用户注册来举例子,会员中心监听用户注册消息,然后创建会员,并抛出会员创建消息,下游系统如等级中心、积分中心、营销中心、通知中心等如果有相关业务就订阅会员创建消息,进行消费,实现对应初始化等级积分、发放优惠券、发送通知逻辑。
  • 上游系统也不需要考虑数据结构兼容性、消费异常处理、是否重试等。上游系统只需要定义抛出消息的内容即可,不需要关注类型转换(这是下游系统也就是消费者需要做的事情),消费者需要什么字段就去消息体中取。
  • 下游系统消费异常不会影响上游业务。在一般的 MQ 消息场景中上游系统和下游系统并不处于同一事务,下游系统的异常也不会影响上游系统的正常运行,我们通常通过消息重试、消息补偿逻辑来保证下游系统和上游系统的数据一致性,即保证下游系统消费消息成功。

在使用消息队列的情况下,用户注册业务流程图如下:

这样通过消息队列这个中间件就实现了等级中心、积分中心和会员中心的解耦。

1.2 异步

还是拿上面的注册业务来举例,假设现在的业务是用户成功注册后,系统会自动为其发放新人优惠券,我们暂且不考虑解耦中提到的各种不合理之处。

在不使用消息队列的情况下,此业务是通过同步接口调用来进行的,流程图如下:

我们假定用户注册、创建会员、发放新人优惠券的逻辑耗时都是50ms,那么在当前业务下整体接口耗时就是150ms,无论是 ToC 还是 ToB,这样的指标对于服务提供方来说是不及格的,一般我们期望接口的整体 RT(Response Time, 响应时间) 是在 100ms 以内。

可能有人会说,可以对每个接口进行优化,让 RT 保持在 20ms 以内,且不说这实际可行可不行,就算每个接口的 RT 是 20ms,假设我现在又有初始化会员等级、初始化会员积分账户、发送短信等等业务需要接入呢?即便每个接口的 RT 都能保持在 20ms 以内,一旦数量增加,总有一天整体 RT 会达到一个瓶颈再也无法优化了。

所以,在异步这一点上来说,我们认为接口业务的复杂性导致了整体 RT 过高,可以将部分非核心业务异步执行,以此来达到提高主流程 RT 的目的

即若使用消息队列,上游系统只需要考虑核心业务的 RT 即可,其他非核心业务可以异步执行。

1.3 削峰

对于削峰的解释一般是将请求写入MQ,从而防止大量请求进入瞬间将 MySQL 打爆的情况,同时也可以一定程度上提高接口的QPS。

但是在实际 ERP 系统的项目中,我们一般不会直接将请求写入 MQ,而是在经过一定计算后,先算出结果进行持久化,然后再在异步逻辑中将过程中需要持久化的内容经过计算再持久化。例如:在积分商城的积分兑换业务中,用户可以使用积分兑换实物,我们先校验用户是否有足够多的积分,然后直接扣减其积分账户的可用数额,然后通过消息队列异步执行具体哪些天通过签到、消费获得的积分被扣除了。

其实这样做最主要的目的还是为了提高该接口的 TPS。

除此之外,在秒杀业务中,我们可以考虑通过 MQ 来防止过多的用户请求:如果用户请求数量大于队列最大长度,直接抛弃用户请求或抛出业务性异常,业务流程图如下:

MQ-1-1.png

2. 消息队列优缺点

2.1 优点

消息队列的优点就是上述的三个:

  • 解耦
  • 异步
  • 削峰

2.2 缺点

消息队列的缺点,其实在解耦部分中有过一些描述,这里坐了下整理:

  • 系统可用性降低。引入MQ依赖,一旦MQ挂了,整个系统将全线崩溃
  • 系统复杂度提高。引入MQ虽然可以解耦异步削峰,但也带来了新的问题:如何防止重复消费、消息丢失怎么办、如何保证消息的顺序性
  • 引入一致性问题。若消息消费失败,消息生产者与消费者数据就产生了不一致的情况

3. 参考

最后,本文收录于个人语雀知识库: 我所理解的后端技术,欢迎来访。