3万字聊聊什么是RocketMQ(一)

261 阅读10分钟

大家好,我是Leo。

这是开端的第三次循环了。当前正在正处于RocketMQ基础原理。

4万字聊聊阿里二面,保证你看不完

聊聊Redis面试题

3万字聊聊什么是Redis(完结篇)

3万字聊聊什么是MySQL(初篇)

2万字聊聊什么是秒杀系统(中)

为什么需要消息队列?

一个大型的系统中,随着业务量,数据体量逐渐变的复杂。越来越多的模块耦合在一起,相互调用,只要有一块出问题,几乎都是致命的。

消息队列可以解决哪些场景?

异步处理

电商系统的整个流程还是非常长的,如果所有流程都同步执行的话。肯定是保证不了那么高的并发的。所以可以借助 RocketMQ 只要风险管控,库存锁定之后,把当前订单信息写入到 RocketMQ 即可返回客户端。

写到客户端之后,订单服务,用户服务,通知服务等会根据 Topic+Tag 进行消费。这步也就是 异步处理。可以提升并发量。

流量控制

流量控制也就是安全问题,如何防止用户过多的请求压垮我们的秒杀系统。没加消息队列过滤的请求流程图是上述的。加了MQ过滤之后可以看如下图。

  1. 网关在收到请求后,将请求放入请求消息队列
  2. 后端服务从请求消息队列中获取 APP 请求,完成后续秒杀处理过程,然后返回结果。

如果MQ堆积了大量的秒杀请求没有处理,说明当前的后端能力小于请求压力。这个时候后端不会宕机,他会按照自己的节奏尽快的处理完MQ的所有消息。当MQ有空闲的地方空出来之后,他又会继续接收请求。

对于超时的请求可以直接丢弃,APP 将超时无响应的请求处理为秒杀失败即可。运维人员 还可以随时增加秒杀服务的实例数量进行水平扩容,而不用对系统的其他部分做任何更改

如果没有MQ作为流量控制的话,所有的请求都打到后端,后端就会宕机。

这里MQ堆积下面会详细介绍。

这里还可以通过令牌桶的思路来达到相同的效果。而且令牌桶的实现更简单。

令牌桶控制流量的原理是:单位时间内只发放固定数量的令牌到令牌桶中,规定服务在处理 请求之前必须先从令牌桶中拿出一个令牌,如果令牌桶中没有令牌,则拒绝请求。这样就保 证单位时间内,能处理的请求不超过发放令牌的数量,起到了流量控制的作用

实现的方式也很简单,不需要破坏原有的调用链,只要网关在处理 APP 请求时增加一个获 取令牌的逻辑

令牌桶可以简单地用一个有固定容量的消息队列加一个“令牌发生器”来实现:令牌发生器 按照预估的处理能力,匀速生产令牌并放入令牌队列(如果队列满了则丢弃令牌),网关在 收到请求时去令牌队列消费一个令牌,获取到令牌则继续调用后端秒杀服务,如果获取不到 令牌则直接返回秒杀失败

服务解耦

先看下面两张图。一个是单体结构,一个是微服务+MQ架构。能很清晰度看出来,第二张图简单一些。就是借助的是MQ的解耦特性。为后续的维护,新增新需求,新功能。都是非常优秀的。

不管系统怎么变,任何系统都没有联系,唯一有联系的就是MQ。 系统生产系统只发给MQ,系统消费消息也是只从MQ中消费。

MQ如何选择?

常用的消息队列

  • Kafka:吞吐量大概在10 万级,高吞吐,支持topic。一般配合大数据类的系统来进行实时数据计算、日志采集等场景。延迟在 ms 级以内。它的可用性非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用。可以做到0丢失
  • RocketMQ:10 万级,支撑高吞吐,支持topic。延迟在ms 级,可用性非常高,分布式架构。 经过参数优化配置,可以做到 0 丢失
  • ActiveMQ:吞吐量大概在万级,延迟在ms级。可用性高,基于主从架构实现高可用。有较低的概率丢失数据。
  • RabbitMQ:吞吐量大概在万级,延迟在微秒级,这是 RabbitMQ 的一大特点,延迟最低。可用性高,基于主从架构实现高可用。基本不丢数据

优点

上个篇幅已经介绍了。

缺点

  • 系统可用性降低: 俗话说,写的越多,出错的几率越大。
  • 系统复杂度提高:原本调用就够了,现在要接入MQ,还要做一些参数的配置,性能调优配置。加大的开发难度。
  • 一致性问题: 宕机后MQ未处理的消息以及已处理的消息,和数据库,和Redis的数据一致性问题

个人选型

我的技术选型是RocketMQ,主要原因如下

  1. 吞吐量在10万级,延迟低,可以做到0丢失
  2. 支持集群,确保不会因为某个节点宕机导致服务不可用,当然也不能丢消息
  3. 可用性很高,体量到了一定级别,可以采用分布式架构进行扩展
  4. 底层是用Java开发的,故障排查,二次开发也是非常友好的(Java程序员)
  5. 阿里开源的,社区较为活跃,出现问题也方便解决

这里后续,经验丰富的时候会回来继续深入,技术选型。这里没那么简单!

主题和队列

最初的消息队列就是我们平时简单使用的 Queue,随着架构的演变,标准化跟不上演进速度,甚至到最后被废弃。

那么这个章节描述的主题和队列是什么?主题和队列又有什么关系?

队列模型

最初的队列模型就是 简单的先进先出(FNFO)

业务变得越来越复杂之后,出现了多个生产者向一个消息队列里发送,然后又有多个消费者向一个消息队列里接收。这些消费者之间实际上是竞争的关系,每个消费者只能收到队列中的一部分消息,也就是说任何一条消息只能被其中的一个消费者收到。

如果需要将一份订单消息数据分发给多个消费者(用户服务,通知服务),要求每个消费者都能收到全量的消息,这个时候,单个队列就满足不了需求,一个可行的解决方式是,为每个消费者创建一个单独的队列,让生产者发送多份

显然这种方法是不可取的,违背了解耦的目的。而且每一份数据都会copy了好多份。极大的占用了内存。

为了解决这个问题,演化出了另外一种消息模型:发布 - 订阅模型

在发布 - 订阅模型中,消息的发送方称为发布者(Publisher),消息的接收方称为订阅者 (Subscriber),服务端存放消息的容器称为主题(Topic)。发布者将消息发送到主题 中,订阅者在接收消息之前需要先“订阅主题”。“订阅”在这里既是一个动作,同时还可以认为是主题在消费时的一个逻辑副本,每份订阅中,订阅者都可以接收到主题的所有消息。

在这种发布 - 订阅模型中,如果只有一个订阅者,那它和队列模型就基本是一样 的了。也就是说,发布 - 订阅模型在功能层面上是可以兼容队列模型的。

RocketMQ消息模型

RocketMQ消息模型采用的是标准的发布-订阅模型,在 RocketMQ 的术语表中,生产者、消费者和主题与我在上面 讲的发布 - 订阅模型中的概念是完全一样的。

RocketMQ也有队列的概念,在RocketMQ中是非常重要的,我们可以先看看消费机制。

消费机制: 几乎所有的消息队列都使用一种,简便的**(请求,确认)**机制 。确保消息不会在传递过程中由于网络或服务器故障丢失。具体的做法也非常简单。

在生产端,生产者先将消息 发送给服务端,也就是 Broker,服务端在收到消息并将消息写入主题或者队列中后,会给生产者发送确认的响应。

如果生产者没有收到服务端的确认或者收到失败的响应,则会重新发送消息;在消费端,消费者在收到消息并完成自己的消费业务逻辑(比如,将数据保存到数据库中)后,也会给服务端发送消费成功的确认,服务端只有收到消费确认后,才认为一条消息被成功消费,否则它会给消费者重新发送这条消息,直到收到对应的消费成功确认

如下图

这套机制很好地保证了消息传递过程中的可靠性,但是,引入这个机制在消费端带来了一个很大的问题!

为了确保消息的有序性,在某一条消息被成功消费之前,下一条消息是不能被消费的,否则就会出现消息空洞,违背了有序性这个原则

也就是说,每个主题在任意时刻,至多只能有一个消费者实例在进行消费,那就没法通过水平扩展消费者的数量来提升消费端总体的消费性能。为了解决这个问题,RocketMQ 在主 题下面增加了队列的概念

每个主题包含多个队列,通过多个队列来实现多实例并行生产和消费。需要注意的是, RocketMQ 只在队列上保证消息的有序性,主题层面是无法保证消息的严格顺序的。

RocketMQ 中,订阅者的概念是通过消费组(Consumer Group)来体现的。每个消费组 都消费主题中一份完整的消息,不同消费组之间消费进度彼此不受影响,也就是说,一条消 息被 Consumer Group1 消费过,也会再给 Consumer Group2 消费。

消费组中包含多个消费者,同一个组内的消费者是竞争消费的关系,每个消费者负责消费组 内的一部分消息。如果一条消息被消费者 Consumer1 消费了,那同组的其他消费者就不 会再收到这条消息。

在 Topic 的消费过程中,由于消息需要被不同的组进行多次消费,所以消费完的消息并不 会立即被删除,这就需要 RocketMQ 为每个消费组在每个队列上维护一个消费位置 (Consumer Offset),这个位置之前的消息都被消费过,之后的消息都没有被消费过, 每成功消费一条消息,消费位置就加一。这个消费位置是非常重要的概念,我们在使用消息 队列的时候,丢消息的原因大多是由于消费位置处理不当导致的。

非常欢迎大家加我个人微信有关后端方面的问题我们在群内一起讨论! 我们下期再见!

长按上方扫码二维码,加我微信,拉你进群