在面试大型互联网公司的时候,很可能会被问到消息队列的问题:
1. 在何种场景下使用了消息中间件?
2. 为什么要在系统里引入消息中间件?
3. 如何实现幂等?
链式调用是我们在写程序时候的一般流程,为了完成一个整体功能,会将其拆分成多个函数(或子模块),比如模块 A 调用模块 B,模块 B 调用模块 C,模块 C 调用模块 D。但在大型分布式应用中,系统间的 RPC 交互繁杂,一个功能背后要调用上百个接口并非不可能,这种架构有如下 几个劣势:
1、这些 接口之间耦合比较严重,每新增一个下游功能,都要对上有的相关接口进行改造;举个例子:假如系统 A 要发送数据给系统 B 和 C,发送给每个系统的数据可能有差异,因此系统 A 对要发送给每个系统的数据进行了组装,然后逐一发送;当代码上线后,新增了一个需求:把数据也发送给 D。此时就需要修改 A 系统,让他感知到 D 的存在,同时把数据处理好给 D。在这个过程中你会看到,每接入一个下游系统,都要对 A 系统进行代码改造,开发联调的效率很低。其整体架构如下图:
2、面对大流量并发时,容易被冲垮 。 每个接口模块的吞吐能力是有限的,这个上限能力如果堤坝,当大流量(洪水)来临时,容易被冲垮。
3、存在性能问题。RPC 接口基本上是同步调用,整体的服务性能遵循“木桶理论”,即链路中最慢的那个接口。比如 A 调用 B /C/ D 都是 50ms,但此时 B 又调用了 B1,花费 2000ms,那么直接就拖累了整个服务性能。
根据上述的几个问题,在设计系统时可以明确要达到的目标:
1、要做到系统解耦,当新的模块接进来时,可以做到代码改动最小;
2、设置流量缓冲池,可以让后端系统按照自身吞吐能力进行消费,不被冲垮;
3、强弱依赖梳理,将非关键调用链路的操作异步化,提升整体系统的吞吐能力,比如上图中 A、B、C、D 是让用户发起付款,然后返回付款成功提示的几个关键流程,而 B1 是通知付款后通知商家发货的模块,那么实质上用户对 B1 完成的时间容忍度比较大(比如几秒之后),可以将其异步化。
在现在的系统视线中,MQ 消息队列是普遍使用的,可以完美的解决这些问题的利器。下图是使用了 MQ 的简单架构图,可以看到 MQ 在最前端对流量进行蓄洪,下游的系统 ABC 只与 MQ 打交道,通过事先定义好的消息格式来解析。
引入 MQ 之后的系统架构、交互方式与最初的链式调用架构非常不同,虽然可以解决上文提到的问题,但也要充分理解其原理特性来避免其带来的副作用,这里以消息队列如何保证“消息的可靠投递”为切入点,来看看 MQ 的实现方式。
1. Client 如何将消息可靠投递到 MQ
1.Client 发送消息给 MQ
2.MQ 将消息持久化后,发送 Ack 消息给 Client,此处有可能因为网络问题导致 Ack 消息无法发送到 Client,那么 Client 在等待超时后,会重传消息;
3.Client 收到 Ack 消息后,认为消息已经投递成功。
2. MQ 如何将消息可靠投递到 Client
1.MQ 将消息 push 给 Client(或 Client 来 pull 消息)
2.Client 得到消息并做完业务逻辑
3.Client 发送 Ack 消息给 MQ,通知 MQ 删除该消息,此处有可能因为网络问题导致 Ack 失败,那么 Client 会重复消息,这里就引出消费幂等的问题;
4.MQ 将已消费的消息删除
关注公众号:java 宝典