消息队列应用场景与常见消息中间件对比(附:秒杀场景、订单超时取消)
1,什么是消息队列?
- 消息队列(Message Queue)是一种进程间或者线程间的异步通信方式。
- 使用消息队列,消息生产者在产生消息后,会将消息保存在消息队列中,直到消息消费者来取走它,即消息的发送者和接收者不需要同时与消息队列交互。
- 使用消息队列可以有效实现服务的解耦,并提高系统的可靠性以及可扩展性。
2,消息队列的应用场景
提示:消息队列最鲜明的特征就是异步、削峰、解耦。基本上一切消息队列的应用场景,都是围绕这三个特性来设计的。包括日志处理、消息通讯、秒杀场景和订单超时取消。
(1)应用解耦:系统的耦合性越高,维护性和扩展性就越低。使用消息队列中间件解耦后,系统的维护性和扩展性就会提高。
(2)异步执行:对于一些没有前后依赖关系的业务计算逻辑,不要使用同步的方式执行,这样太浪费时间。可以考虑使用异步的方式执行,加快响应速度。
3,常见的消息队列中间件
常见的消息队列中间件有很多,包括 Kafka、ActiveMQ、RabbitMQ、RocketMQ 等。下表对这几个常见的消息队列中间件进行了对比。
提示:对大数据应用场景,主要考虑的是消息的高吞吐量和稳定性,所以 Kafka 是最合适的。
对比项 | Kafka | ActiveMQ | RabbitMQ | RocketMQ |
来源 | Apache | Apache | RabbitMQ | 阿里巴巴捐赠给Apache |
开发语言 | Scala | Java | Erlang | Java |
架构 | 分布式 | 主从 | 主从 | 分布式 |
吞吐量 | 百万级别 | 万级 | 万级 | 十万级别 |
时效性 | ms 级以内 | ms 级以内 | us 级 | ms 级 |
稳定性 | 非常高 | 高 | 高 | 非常高 |
社区活跃度 | 高 | 中 | 高 | 中 |
资料完整度 | 高 | 高 | 高 | 高 |
功能特点 | 只支持主要的 MQ 功能,最重要的特点是吞吐量高,主要应用在大数据场景中 | 成熟的产品,应用比较多,对各种协议的支持比较好 | 并发能力很强,性能极好,延时很低,管理界面较丰富 | 功能比较全,扩展性佳 |
附一:不使用消息队列会带来什么问题?
同步调用方案相比引入消息队列有三个缺陷,分别是性能差、可扩展性差和可用性差:1,性能差
(1)性能差是因为我们需要停下来等全部调用完成才可以返回响应。
- 例如下面业务流程,创建订单后需要通知三个下游,那么就需要发起三次调用,并且等它们各自的结果返回之后才能继续往下执行,或者返回响应,这样性能太差了。
(2)虽然我们可以开启多个线程或者协程,并发调用所有的下游,从而缩短整体的响应时间。当时并发调用相比于使用消息队列,性能也更差。
- 在并发调用的情况下,性能取决于最坏的那个同步调用什么时候返回结果。而正常我们丢一个消息到消息中间件上是很快的。
- 并且,即便并发调用的性能损耗是可以接受的,但是扩展性和可用性还是解决不了。
2,扩展性差
(1)在使用消息队列的情况下,消息发送者完全不关心谁会去消费这些消息。同样地,如果有一个新的业务方要订阅这个消息,它可以自主完成。
(2)而同步调用的时候,上游必须知道下游的接口,然后要知道如何构造请求、如何解析响应,还要联调、测试、上线,整个过程都得和下游密切合作,因此效率特别低,可扩展性很差
提示:如果在某些场景下确实不能用消息队列,那么这个扩展性问题可以通过一些技术手段来缓解。比如说上游提供一整套的对接规范,包括 API 定义、请求和响应中每个字段的含义。这样下游就对着这个 API 定义来提供实现,上游就不需要适配每一个下游了。这是对接众多下游的基本设计,可以充分保障高可扩展性和高研发效率。
3,可用性差
(1)在使用消息队列的方案中,我们只需要确保自己把消息发送到了消息队列上,就认为操作已经成功了。
(2)但是,在同步调用方案中,我们必须要确保调用所有的下游都成功了才算是成功了。所以我们还需要额外考虑部分成功部分失败的问题。所以相比使用消息队列的方案,同步调用的方案更加容易出错,并且容错也更难。
附二:消息队列在秒杀场景中的应用
1,秒杀场景的实现方案
(1)一般秒杀的架构设计中都会使用消息队列,同时利用消息队列三个特性(异步、削峰、解耦)。下面是一个比较简单的秒杀架构图:
- 在消息队列之前,要对用户请求做一些校验,比如说这个用户是否已经参加过秒杀了。其次要扣库存,扣库存成功才算是抢到了。
- 紧接着就是把这个请求丢到消息队列里,后续异步创建订单,并且提示用户支付,最后完成支付。
(2)这种设计的精髓就是利用消息队列把整个秒杀过程分成轻重两个部分:
- 在进入消息队列之前的操作都是轻量级的(限流、请求校验和库存扣减等),一般也就是内存计算或者访问一些 Redis,所以我们可以认为瓶颈基本上取决于 Redis 的性能。
- 而进入消息队列之后就是非常重量级的操作了,比如说要进一步验证交易的合法性,操作数据库等。
2,订单超时取消的实现方案
(1)在电商里面,如果用户下单之后一直没有支付,那么这个订单就会被取消,从而释放库存。
(2)订单超时取消在行业内有很多种做法,如果要想利用消息队列实现订单超时取消功能,需要使用延时消息。所谓的延时消息,就是发送者在发送之后,要过一段时间,消费者才能消费的消息。
- 比如:我们可以准备一个延时队列,超时时间是 30 分钟,那么延时也是 30 分钟。
注意:目前主流的消息队列中 RocketMQ 是支持延时消息的,它有插件支持。但是 Kafka 不支持。
(3)但是消费的时候要小心并发问题,就是在 30 分钟这一个时刻,一边用户支付,一边消费者也消费超时消息,就会有并发问题。解决思路有很多,可以使用分布式锁、乐观锁,也可以使用 SELECT FOR UPDATE 锁住订单,防止并发操作。
乐观锁方案就是在我们把订单更新为超时状态的时候,需要确保原始状态还是未支付。
- UPDATE `order` SET `status`="超时未支付" WHERE `id`=123 AND `status`="未支付"
附三:基于事件驱动的 SAGA 分布式事务方案
1,事件驱动
(1)事件驱动(Event-Driven)可以说是一种软件开发模式,也可以看作是一种架构。它的核心思想是通过把系统看作一系列事件的处理过程,来实现对系统的优化和重构。事件驱动适合用来解决一些复杂、步骤繁多、流程冗长的业务问题。
- 我们可以直观地理解成,整个系统不同组件之间的通信是通过事件来完成的。也就是组件 1 发送一个事件到消息队列上,然后组件 2 消费这个消息。组件 2 消费完成后再发出一个消息到消息队列。每一个事件就是一个消息。
- 这些消息可能有不同的 Topic,也可能发送到不同的消息队列集群。但是毫无疑问它们要通过密切合作来解决一个业务问题。
(2)事件驱动又如下几个优点:
- 低耦合性:各个组件只依赖消息队列,组件之间通过消息的定义间接地耦合在一起。换句话来说,组件只需要知道消息的定义,并不需要知道发送消息的组件是哪个。
- 可扩展性:事件驱动的应用程序具有很强的扩展性,可以通过添加新的事件处理程序、组件等来实现系统的扩展和升级。
- 高可用:可以充分利用消息队列的可靠性、可重复消费等特性,来保证消息发送、消费高可用,从而保证整个系统的高可用。
2,事件驱动与 SAGA
(1)下面是事件驱动和 SAGA 结合之后的形态:
(2)也就是说,当某一个步骤完成之后,就会发出一个或者多个事件,驱动事务中的后续步骤。包括回滚也是这样,比如说发出一个代表某一个步骤执行失败的事件,对应的消费者就会去执行反向补偿步骤。
(3)使用事件驱动的优点是低耦合、高扩展性、异步、高可用。不过在实时性上要比同步调用差一点。
- 比如说我们有一个分布式事务,就是要求先更新 DB,再更新缓存。那么在缓存更新失败的场景下,过程看起来就像图里展示的这样。
注意:其中还原 DB 是指我们需要用原始数据更新回去,而不是数据库回滚操作。当然,这个例子只是帮助你理解,正常来说这么简单的分布式事务是用不着 SAGA 的。