RocketMQ 学习笔记
为什么要学习 RocketMQ?
相信无论是前端还是后端的同学,不管是在平常的自我充电、还是日常的工作中,对 RocketMQ、Kafka 等主流消息队列多多少少都有一定的了解。现在的服务端架构中,一个业务模块中的服务往往会根据一定维度拆分成多个服务。而消息队列大家可以理解为 多个服务、系统之间沟通的通道
,如下图所示:
有了这个通道,系统 A 不再是直接与系统 B 交互,而是与消息队列交互。在异步场景下,通过消息队列这个通道交互,可以在产品侧获得更好的用户体验
。在支持异步的同时,消息队列也能很好地支持系统间的解耦,而系统之间的松耦合能够给系统带来良好的扩展性、可维护性。
消息队列作为现在技术架构中不可或缺的一部分,其重要程度不言而喻,应用也相当广泛。也正是因为这样,各个大厂都在相继自研消息队列,例如最初来自 LinkedIn 的 Kafka
、来自阿里的 RocketMQ
、以及来自 Yahoo 的 Pulsar
。
那面对如此多的消息队列,咱们为啥要选择 RocketMQ 呢?
第一点,开发语言优势。RocketMQ 使用 Java 语言开发,比起使用 Erlang 开发的 RabbitMQ 来说,有着更容易上手的阅读体验和受众。在遇到 RocketMQ 较为底层的问题时,大部分熟悉 Java 的同学都可以深入阅读其源码,分析、排查问题,甚至可以在社区版的基础上进行二次开发
。
第二点,丰富的高级特性。根据 RocketMQ 官方文档的列举,其高级特性达到了 12 种
,例如顺序消息、事务消息、消息过滤、定时消息等,这些特性在后续我们都会进行讲解。RocketMQ 丰富的特性,能够为我们在复杂的业务场景下尽可能多地提供思路及解决方案。
第三点,良好的商业前景。比如阿里,在其生产环境中已经部署了数百个 RocketMQ 集群,上千个节点
,如此大量地将 RocketMQ 应用于生产环境,足以说明 RocketMQ 是的确经得起残酷的生产环境考验的,并且能够针对线上环境复杂的需求场景提供相应的解决方案。
RocketMQ 不仅仅解决了阿里的内部需求,同时还被搬到了阿里云上,作为一个商业化的产品
对外提供服务。在阿里云的产品叫作“消息队列 RocketMQ 版”,自从 2016 年开始商业化到现在,RocketMQ 商业化版已经具有相当大的规模。良好的商业前景,也反向推动着 RocketMQ 在业界的普及,两者相辅相成、相得益彰。
第四点,众多大厂背书。RocketMQ 现在被广泛应用于各个大厂的内部业务中,其诞生之地阿里自不必多说,历年来大家所熟悉的双十一促销,在阿里内部就是使用的 RocketMQ 来承载消息
,面对如此庞大的流量洪峰,RocketMQ 交出了一份令人满意的答卷。再比如,RocketMQ 也是阿里交易链路中的核心产品。交易链路本已是核心中的核心,而这个核心又将 RocketMQ 当作核心链路中的核心,足以见对 RocketMQ 的重视程度。
同时,字节跳动
内部不同的业务也都在大量地使用 RocketMQ,同样也是作为业务主流程中的核心组件,RocketMQ 在生产环境中保持着非常高的稳定性、可用性,非常良好地支撑了业务的运行与发展。
一门技术,有大厂背书意味着什么?
- 意味着,这个技术经历过严酷的生产环境考验,非常可靠。
- 意味着,你在开发中可能遇到的问题,大厂大概率都会、甚至已经遇到,并且大概率都会提供相应的解决方案。
- 意味着,这门技术大概率会更加地流行,而更流行又会让社区越来越活跃,社区越活跃,相应业务场景“开箱即用”的解决方案就越多,形成良性循环。例如,Java 的社区、生态非常好,非常多的业务场景你甚至能直接找到封装好的工具。
- 意味着,如果你能熟练掌握这门技术,而对应的公司恰好也在使用这门技术,可能会让你的求职更加顺利。
所以说,RocektMQ 有着不可多得的先发优势——所谓的大厂背书。
学习原理类课程的痛点
看似我们看了非常多的模块、非常多的文件、非常多的代码,但是实际上等我们看完之后会发现,我们的收获就跟上面这个水杯里的水一样,空空如也。看到这是不是有那么些许的悲惨?花了这么多时间学习,投入这么多精力,完了还没有收获。
之所以出现上面这个情况,是因为我们在不熟悉一个框架的情况下,就深入到其业务代码中去阅读源码,看似思路很清晰、目的很明确,实则会深陷入代码实现的无限细节当中无法自拔。
我们可能刚刚开始看时能够保持清醒,但再往源码深入几层,随着其内部函数的调用跳来跳去,很快就会被“绕晕“的。
RocketMQ 核心的概念
Broker:消息队列的核心
从这里开始,我们需要把整个消息队列当成一个黑盒子来看待。
啥叫黑盒子?举个例子,自动贩卖机对我们来说,就是个黑盒子。因为从我们的视角来看,操作就是:给现金或者扫码线上支付,然后机器就会给到你对应的商品。具体里面是机器在自动操作,还是活人在半自动操作,我们不得而知,也并不关心。
参考这个例子,消息队列作为一个黑盒子应该是个啥样呢?或许应该是这样:
Producer(生产者)只管投递消息到消息队列中,而 Consumer(消费者)则负责从消息队列中消费消息,这是符合大家对消息队列最初印象的工作流程。至于其内部如何实现的,我们现在也看不到,因为它目前是个黑盒子,内部的相关实现是没有暴露出来的。
RocketMQ 是一个内部拥有多个组件、且组件之间相互配合协作良好的精密机器,我们看到的只是封装之后的“假象”。实际上提供服务的叫作 Broker
,你可以把它理解成消息队列的一个实例:
Broker 可以说是 RocketMQ 核心中的核心,简单来说它负责接收 Producer 发送过来的 Message 、并将其持久化起来,同时负责处理 Consumer 的消费请求。上图提到的完整工作流程:投递 Message、存储 Message、处理消费请求等,Broker 都参与了。
当然,任何的框架、服务,只要是单节点部署就会有这个问题。所以,为了保证整个架构的高可用,RocketMQ 会部署多台 Broker 组成一个集群
对外提供服务,如下图所示:
而 Producer 生产的消息会分布式存储在这些 Broker 上。啥叫分布式存储呢?举个例子,大家应该都有存钱在银行,假设 A 银行、B 银行都有存,那么 A 和 B 银行中的存款全部加起来,就是你的总存款。
消息队列的消息也是同理,消息分散
存储在多台 Broker 上,每台 Broker 实际上存储的消息内容是不同的,将这些 Broker 存储的消息全部加起来就是全部的数据,如下图所示:
有的同学看到这里可能又会有问题了:“既然每台 Broker 存储的消息不同,那如果某台 Broker 突然整个挂了,这部分消息不就丢失了吗?就这还高可用呢?”
别急,我们继续往下看。
Topic:消息的组织者
如果 RocketMQ 真是这样设计的,那的确会有问题。当然,实际上肯定不是这样搞的。在 RocketMQ 中有 Topic 的概念:表示一类或者说一大类消息的集合
。
举个例子,假设某个 Topic 中存的全是跟订单相关的,那么里面的消息就有可能是:订单已创建
、订单已更新
、订单已付款
……诸如此类的消息。
并且,Topic 在 RocketMQ 底层设计中其实是个抽象概念。Producer 在生产消息时,会指定将 Message 投递到某个 Topic,Consumer 消费消息时,也会指定一个 Topic 进行消费,如下图所示:
这个时候,对于 Producer、Consumer 来说,消息队列就是个黑盒。它们只关心投递 Message 到指定的 Topic,再从指定的 Topic 消费,其余的一概不关心。
但其实上面的图画得不是很准确,因为我们从上图中能够分析出一个结论:一个 Topic 只会存储在一台 Broker 上。你可以思考一下,真的是这样吗?
当然不是,这个结论是错误的。这里我们从两个方面论证一下。
- 第一个方面,假设 Topic A 和 Topic C 的 Message 体量在业务上非常小,而 Topic B 的数据量非常大,那么就会导致 Broker 的负载、存储压力都更大,导致严重的
数据倾斜问题
。 - 第二个方面,这样的设计不具备高可用性,当 Broker B 意外宕机,也就意味着 Topic B 中的 Message 会全部
丢失
。
所以,为了让大家更好地了解正确的底层原理,我们还需要引入新的组件来帮助我们解惑。
MessageQueue:对 Topic 的再次细分
我们简单举一个例子来了解 MessageQueue。假设现在有 Topic A
这个 Topic,它有 3 个 MessageQueue,ID 分别为 0、1、2,那么用图来表示大概就是这样:
分散的 MessageQueue 一起组成了一个完整的 Topic。同 Topic 一样,MessageQueue 也是一个逻辑上的概念,你可以把它理解成对底层存储的抽象。
你也许看到过说 Message 会被丢到 MessageQueue 当中,但实际上 Message 最终会被存储在 Broker 所在机器的磁盘上,那里才是 Message 的”最终归宿”,如下图所示:
了解了 MessageQueue 的概念之后,同一个 Topic 下的 Message 会分散存储在各个 Broker 上,一来能够最大程度地进行容灾,二来能够防止数据倾斜。这下“鸡蛋不在一个篮子里了”,数据被较为均匀地分摊了出去,出现数据倾斜的概率也大大降低了。
不过眼尖的同学可能发现了:“就算是引入了 MessageQueue 让数据分散存储了,Broker B 如果挂了,数据该丢还得丢啊,之前只丢一个 Topic 的 Message,现在倒好,3 个 Topic 的数据都会丢。“
没错,按照上面的架构图我们得到的结论的确是这样。但实际上,RocketMQ 4.5 版本之前提供了基于主从架构的高可用机制。即将 Broker 按照角色分为 Master Broker
和 Slave Broker
,主从之间会定期地进行数据同步,就像这样:
Master 负责响应客户端的读写请求、存储消息等,而 Slave 只负责一件事:同步 Master 的数据。
而在 RocketMQ 4.5 之后,RocketMQ 提供了高可用集群的实现 —— Dledger
。上面讲的主从架构虽然能够做到主从之间的数据同步,但 Master Broker 挂了还是会让集群、数据受损,并且需要人工将 Slave Broker 切换成 Master Broker,然后集群才能正常提供服务。
熟悉 Redis 的同学应该可以做个类比。Redis 中的主从架构也是需要手动地使用 slave of
命令来使一台 Redis 去 Follow 指定的 Redis。这种架构下 Master Redis 出现问题,仍然需要手动切换。而后续 Redis 有了 Sentinel,它是对 Redis 集群高可用的解决方案,它的一大特性就是能够做到在发生故障时自动进行故障转移。
Dledger 也会在发生故障时进行自动切换,其使用 Raft 协议重新选举出一个新的 Broker 重新对外提供服务,不需要人工介入。
我们言归正传,上面引出了 MessageQueue 的概念之后,我们看似已经解决了所有的问题。但很遗憾,这只是一个假象,从概念上的确自洽了,但如果我们把上面的架构代入到真实的开发场景下,就会很容易发现问题。
例如,这里有多台 Broker 组成一个集群在对外提供服务,当 Producer 来建立连接时,应该选择哪台 Broker 呢?
目标 Broker 的 IP 地址是要 Hard Code 到项目的配置文件吗?如果真的这么配置了,那配置的这台 Broker 突然挂掉了呢?难道我们还需要去改项目的配置文件吗?这明显是不靠谱的做法,就算是做成动态配置不用重新发布服务,那也需要一定的时间修改、等待生效。万一它抽风凌晨挂了呢?真要是凌晨应该还好,怕就怕它在业务最高峰期突然挂掉。
所以,为了回答这个问题,我们还需要继续引入新的组件 —— NameServer。
NameServer:RocketMQ 集群的大脑
NameServer 用于存储整个 RocketMQ 集群的 元数据
,就像 Kafka 会采用 Zookeeper 来存储、管理集群的元数据一样。可能有小伙伴第一次听到元数据,对元数据这个概念拿不准,这里简单给大家解释一下。
就拿我们人来举例子。有个同学叫张三,像他的姓名、性别、身高、出生年月日、学历是啥、从哪儿毕业、在哪儿上班、甚至左撇子还是右撇子,都算张三这个个体的元数据。
类比张三,NameServer 中存放的元数据大概就是:
- 集群里都有哪些 Topic?
- 这些 Topic 的 MessageQueue 分别在哪些 Broker 上?
- 集群里都有哪些活跃的 Broker?
那 NameServer 都是怎么知道这些信息的呢?这些信息不会凭空出现在 NameServer 中,毕竟说到底它也只是个服务。当然是 Producer、Consumer、Broker 自行将数据注册到 NameServer 的。
Broker 在启动时会将自己注册到 NameServer 上,并且通过心跳
的方式持续地更新元数据。此外,Producer、Consumer 都会和 NameServer 建立连接、进行交互来动态地获取集群中的数据,这样就知道自己该连接哪个 Broker 了。
我们来看看集成了 NameServer 的架构会变成什么样:
这次看上去好像真的没啥问题了,Broker 动态地将自己注册到 NameServer 上,Producer、Consumer 也能够感知到集群的整个信息。
不过眼尖的同学又有发现了:“你这个 NameServer 是个单点啊,NameServer 可以说是 RocketMQ 集群的大脑,如果 NameServer 挂掉了,岂不是整个集群都将无法正常工作。“
没错。所以实际的生产环境中,会部署多台 NameServer 组成一个集群对外提供服务,如下图所示:
NameSever 可以理解成一个无状态的节点。
看到这你可能会问:“这里面不是存储了很多的元数据吗?咋还能算无状态的节点呢?”这是因为 Broker 会将自己注册到每一个 NameServer 上,每个 NameServer 实例上都有完整的数据,所以可以将这一堆的 NameServer 看成是一个无状态的大“单体”应用。这样的多副本部署保证了 RocketMQ 中扮演大脑角色的 NameServer 的高可用,进而保证了整个 RocketMQ 的高可用。
总结
对于 Topic,在 RocketMQ 的底层设计中它是一个逻辑上的概念。而面对用户(也就是我们)时,它却是一个实打实的需要关注的概念。生产时需要关心,消费时也需要关心。关于 Topic,你可以理解为 RocketMQ 将底层负责的存储、调度的相关设计给封装了起来,让我们在使用的同时不用去关心过多其他的概念。
对于 Broker,它将从 Topic 中细分出来的 MessageQueue 分布式地存储在多台实例上。而 NameServer 则是整个 RocketMQ 集群的大脑,它几乎啥都知道,并且 NameServer 通过多实例的部署保证了自身的高可用。
Producer 在发送消息前从 NameServer 中获取 Topic 的路由信息也就是发往哪个 Broker,Consumer 也会定时从 NameServer 获取 Topic 的路由信息。