搞懂什么是RocketMQ

105 阅读18分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

前言

我们在上一篇中给大家介绍了系统中引入消息队列的必要性,也了解了一些消息队列的基础知识,我们也提到了一些比较常见的问题,并且简单聊了下,那我们在实际的开发中,使用频率最高的消息中间件是哪些呢?

接下来我们介绍RocketMQ,前身是MetaQ,现在叫RocketMQ,阿里这是希望它像中国的嫦娥一样上天可能,还是挺不错的名字

正文

RocketMQ的项目架构

由于RocketMQ是Java开发的,我们也便于读懂源码以及解决问题,先来简单看下它的项目结构:

  • rocketmq-broker:接受生产者发来的消息并存储(通过调用rocketmq-store),消费者从这里取得消息

  • rocketmq-client:提供发送、接受消息的客户端API。

  • rocketmq-namesrv:NameServer,类似于Zookeeper,这里保存着消息的TopicName,队列等运行时的元信息。

  • rocketmq-common:通用的一些类,方法,数据结构等。

  • rocketmq-remoting:基于Netty4的client/server + fastjson序列化 + 自定义二进制协议。

  • rocketmq-store:消息、索引存储等。

  • rocketmq-filtersrv:消息过滤器Server,需要注意的是,要实现这种过滤,需要上传代码到MQ!(一般而言,我们利用Tag足以满足大部分的过滤需求,如果更灵活更复杂的过滤需求,可以考虑filtersrv组件)。

  • rocketmq-tools:命令行工具。

项目源码地址:github.com/apache/rock…

核心组件

其实它内部主要是四大核心组成部分:NameServer、Broker、Producer以及Consumer四部分

NameServer:主要是对元数据的管理,包括Topic和路由信息的管理,底层由netty实现,是一个提供路由管理、路由注册和发现的无状态节点,类似于ZooKeeper

Broker:消息中转站,负责收发消息、持久化消息

Producer:消息的生产者,一般由业务系统来产生消息供消费者消费

Consumer:消息的消费者,一般由业务系统来异步消费消息

理解其中的核心概念

**Message:**Message就是属于要传输的消息,一条消息必须要一个主题topic,等于是通道地址。发送方根据topic发送消息,消费方根据topic来选择相应的消费逻辑。一条消息也可以选择一个Tag标签,理解成topic的子级别,Tag可以不设置,但是topic必须设置,Tag一般用于更方便的查找消息。

**Topic:**Topic也就是上面说的消息的主题,属于消息的第一级别类型,为什么说是第一级别呢,因为还有第二级别Tag。一个消息必须有且只能有一个Topic,Topic和生产者、消费者的关系是解耦合的,

可以由多个生产者向同一个Topic

发送消息,多个消费者也可以同时消费同一个Topic

的消息,也就是被多个消费者订阅。

Tag:

Tag

标签,可以看做子主题属于消息的第二级别的类型,用于为消息提供更多额外的灵活的配置。即Topic

是一级分类,Tag

是二级分类,同一个业务模块中的消息就可以相同的Topic

而不同的Tag

来标识。消息是可以没有Tag

的,标签Tag

等同于是起辅助作用,也可以为查询提供辅助。

Queue

队列:

每个Queue

的内部是有序的,在RocketMQ

中分成了读写两种队列,这和平常我们所见的读写分离不太一样,读写分离一般指的是将一份数据由主节点同步到从节点,主节点可以用于写,从节点用于读,通过主从分离来减轻系统的压力。而RocketMQ

中的读写队列,即读队列和写队列,写入数据的时候按照写队列返回路由信息,读取数据的时候按照读队列个数返回路由信息。比如写队列个数是8

,读队列个数是4

,写入的时候会创建8

个文件夹0

、1

、2

、3

、4

、5

、6

、7

,读取的时候路由信息根据读队列个数返回4

,也就是只读取0

、1

、2

、3

这四个队列中的消息,4

、5

、6

、7

无法消费。同样的道理,如果写队列是4

,读队列是8

,则读取的时候会有四个队列一直没有数据。由此可见,只有读队列数量大于等于写队列的数量,程序才可以正常进行。

最佳实践就是读队列和写队列数量相等,那为什么RocketMQ

还要设置这个,直接强制相等不就可以了吗?其实目的是方便队列的扩容和锁容。我们可以思考一个问题,假设一个Topic

在每个broker

上创建了128

个队列,现需要把队列数量缩减至64

个,如何保证百分百不丢消息,并且无需重启应用?最佳的办法就是先缩小写队列数量0-127

至0-63

,等到64-127

的队列的消息全部消费完之后,再缩容读队列的数量到64

。如果同时缩小读队列和写队列可能会导致部分数据无法消费处理。

Offset

偏移量:

在RocketMQ

中所有的消息队列都是持久化的,队列中的每个存储单元都是定长的,访问其中的存储单元都是通过offset

来访问的,offset

是long

类型的,64

位,理论上百年不会溢出,所以认为是无限长的,即Queue

队列是无限长的数组,offset

是下标。

Group

分组:

可以分为生产者组合消费者组,代表某一类的生产者合消费者,一个Group

可以订阅多个Topic

再次理解核心组成部分

NameServer:主要是对元数据的管理,包括Topic和路由信息的管理,底层是由netty实现,是一个提供路由管理、路由注册和发现的无状态节点,类似于Zookeeper

1、消息调度中心:内部存储着元数据,其中包括Topic和路由信息,平时主要开销是维持心跳和提供Topic-Broker的对应关系,但是有一点需要注意,Broker向NameServer发送心跳的时候,会带上当前自己所负责的所有的Topic信息,如果Topic个数太多,达到万级别,就会导致一次心跳数据达到几十兆,网络情况差的话,网络传输失败,心跳也会失败,导致NameServer误认为Broker心跳失败。

2、路由注册、剔除、发现:每个Broker的启动都会到NameServer注册,Producer发送消息的时候会根据Topic到NameServer上获取到相应的Broker的路由信息进行发送,Consumer也会定时获取Topic的路由信息

由于NameServer和Broker之间维持着心跳机制,可以及时的剔除相应的Broker。由于NameServer和Broker之间维持着心跳机制,可以及时的剔除相应的Broker。

3、集群部署:NameServer集群中的各个节点之间是互相独立的,没有任何的信息交互,被设计成为无状态的,节点之间相互是无通信的,通过部署多台机器来成为一个伪集群。

4、NameServer通信:Broker全量注册所有NameServer节点,生产者和消费者每隔一段时间会发送请求到NameServer(随机节点)上去拉取最新的集群Broker。

如果生产者和消费者向一台已经挂掉的Broker发送或者拉取消息必然是没用的,如何解决?

解决这个问题靠的是Broker和NameServer之间的心跳机制,每个30s,Broker会向NameServer集群里所有节点发送心跳连接,告诉他们自己的最新状态,NameServer接收到之后更新这个Broker最近的一次心跳时间。然后NameServer方面会每隔10s去检查各个Broker的最近一次心跳时间,如果Broker超过120s没有发送心跳了,就认为这个Broker已经挂掉了。此时对于生产者而言,可以控制不发送消息到那一台Broker上,改发到其它Broker上;对于消费者,每个Broker上都有slave的节点备份,可以从slave上拉取消息继续使用。

Broker:消息中转站,负责收发消息、持久化消息,可以把Broker看做是RocketMQ的心脏

1、Broker是提供业务的服务器,每一个Broker节点会与所有的NameServer节点保持着长连接和心跳,同时会带着所有所有的Topic信息注册到NameServer,更新NameServer上的心跳时间

2、Broker提供了消息的接收、存储和拉取等功能,一般需要保证Broker的高可用,所以一般会配置Broker的Slave节点,一个Master可以对应多个Slave,BrokerId为0的代表Master节点,非0的节点是Slave节点,当Master节点挂掉之后,Consumer然后可以去消费Slave

Producer:消息的生产者,一般由业务系统来产生消息供消费者消费

1、消息的生产者,需要与NameServer建立连接,从NameServer获取Topic路由信息,Producer无状态的,由用户集群部署

2、支持三种方式发送消息:同步、异步和单向。其实大家这个大家根据名字也猜出个差不多了,简单解释下,同步发送就是发送方发出数据之后,需要收到接收方的响应之后才会收到下一个数据包,这个一般用于重要的数据。异步发送就是指的是发送数据后,不等待接收方的响应,紧接着可以发送下一个数据,一般用于链路比较长并且对于时间敏感的业务场景。单向发送指的是只负责发送消息而不等待服务器回应,没有回调函数出发,适用于一些对可靠性要求不高的场景。

Consumer:消息的消费者,一般由业务系统来异步消费消息

1、消费者,支持集群消费和广播消费两种模式,集群消费就是单次消费,一个消息由集群中的一个节点消费了,其余的节点就不会再次消费了。广播消费是订阅了的消费者都会进行消费。

2、支持Push和Pull两种消费模式,拉取型消费者会主动从消息服务器拉取消息,批量拉取到消息之后消费者会启动消费过程,属于主动消费型。推送性其实是底层封装了消息的拉取、消费进度和其他的内部维护工作,消息到达时会有一个回调接口来实现,所以push为被动消费型,push在实现的时候首先要注册消费监听器,当监听器触发之后才会开始消费消息。

一次完整的发送和消费流程

首先NameServer肯定是要启动的,单节点或者伪集群启动根据业务场景来定,伪集群是因为NameServer集群之间是没有通信的,就是相互独立的;启动Broker,Broker在启动的时候会先去NameServer注册并且定时发送心跳,心跳的会发送自身所有维护的Topic信息,所以如果一个Broker维护过多的Topic可能会导致心跳时间过长,被NameServer误认为心跳失败

Producer发送消息的时候,会岁集训则NameServer集群中的其中一个节点建立长连接,定期的从NameServer来获取Topic和对应Broker的对应信息,同时向提供对应Topic服务的Broker Master建立长连接,且定时向Broker发送心跳。发送消息的时候,会轮训对应Topic下的所有的队列来发送,实现发送方的负载均衡

Broker在RocketMQ中是处理Producer发送消息请求,Consumer消费消息的请求,并且进行消息的持久化,以及一些服务端的过滤操作,可以理解为集群中的很重要的工作都是交给了Broker进行处理。大家看Broker的源码的话会发现,初始化流程很长,其实就是根据配置创建很多的线程池,线程池来处理对应的发送消息、拉取消息、查询消息、客户端和消费者的管理,同时也有很多定时任务,用来发送拉取消息。

接着就是Consumer消费者进行相应的消费了,Consumer消费者启动之后,会通过定时任务不断地向RocketMQ集群的Broker实例发送心跳包,主要是订阅关系集合和消费者分组名称这些信息。消息拉取就根据业务需求采用Push模式或者Pull模式

  • push模式:客户端与服务端建立连接后,当服务端有消息时,将消息推送到客户端。

  • pull模式:客户端不断的轮询请求服务端,来获取新的消息。

在具体实现的时候,push和pull都是采用消费端主动拉取的方式,即Consumer轮询从broker拉取消息。在push的方式里,Consumer把轮询过程封装了,并注册了MessageListener监听器,取到消息后,唤醒MessageListener的consumeMessage()来消费,对用户而言,感觉消息是被推送过来的。Pull方式里,取消息的过程需要用户自己写,首先通过打算消费的Topic拿到MessageQueue的集合,遍历MessageQueue集合,然后针对每个MessageQueue批量取消息,一次取完后,记录该队列下一次要取的开始offset,直到取完了,再换另一个MessageQueue。

不知道大家有没有想到一些问题,既然都是采用pull方式实现的,那如何保证消息的实时性呢?既然有push模式,为什么要在实现的时候都变成了pull拉取的模式?

首先实时性问题是通过消费者轮询去pull来实现的,至于底层全部采用pull来实现,是因为如果采用push模式,消费者端必须要建立一个长连接,如果消费者过多,会对MQ造成很大的系统压力,但是使用了轮询,消息的实时性也会受到影响,开发的时候采用长轮询来是吸纳的

问题总结

优缺点

RocketMQ优点:

· 单机吞吐量:十万级

· 可用性:非常高,分布式架构

· 消息可靠性:经过参数优化配置,消息可以做到0丢失

· 功能支持:MQ功能较为完善,还是分布式的,扩展性好

· 支持10亿级别的消息堆积,不会因为堆积导致性能下降

· 源码是java,我们可以自己阅读源码,定制自己公司的MQ,可以掌控

· 天生为金融互联网领域而生,对于可靠性要求很高的场景,尤其是电商里面的订单扣款,以及业务削峰,在大量交易涌入时,后端可能无法及时处理的情况

· RoketMQ在稳定性上可能更值得信赖,这些业务场景在阿里双11已经经历了多次考验,如果你的业务有上述并发场景,建议可以选择RocketMQ

RocketMQ缺点:

· 支持的客户端语言不多,目前是java及c++,其中c++不成熟

· 社区活跃度不是特别活跃那种

· 没有在 mq 核心中去实现JMS等接口,有些系统要迁移需要修改大量代码

2、消息重复问题

消息领域有一个对消息投递的服务质量定义,分为最多一次、至少一次、和仅一次三种,现在几乎所有的MQ都说能做到至少一次,既然是至少一次,那肯定就少不了消息去重问题。

比如一些网络中断、服务重启等造成的消息重复发送,既然重复发送了,就需要解决这个问题

有的业务场景,可能天然能够解决这个问题,比如订单数据,订单ID本来存在唯一性,在数据库层面其实就已经保证了幂等性了。幂等性就是用户对于同一操作发起的一次请求护着多次请求的结果都是一致的,不会因为多次请求而产生副作用或者说造成结果不一致的情况。

3、消息的可用性问题

RocketMQ提供了对消息的同步和异步两种刷盘的策略,同步刷盘中,如果刷盘超时会返回FLUSH_DISK_TIMEOUT,如果是异步刷盘则不会返回相应的刷盘信息,选择同步刷盘可以尽最大程度满足我们的消息不会丢失。同步和异步可以保证我们把消息数据存储到一台机器上,但是如果这台机器磁盘损坏呢?弄多台数据备份就好了,于是就有了主从同步

其实我觉得大家对主从同步应该听的耳朵都出茧子了,凡是集群基本都存在主从同步的问题,主从同步有同步和异步两种模式来进行复制,当然同步肯定是可以提高可用性,但是肯定会降低系统的响应能力

至于RocketMQ底层是如何存储的,这个问题我也会单独拎出来一篇来介绍,因为这个问题也是值得详细的说说的,RocketMQ中有一个CommitLog日志文件,Broker单个实例下的所有的队列都公用这个数据日志文件。大家先暂时混个耳熟即可

4、分布式事务问题

这个问题呢,也是分为很多的解决方案的,网上一搜一大把的也是,分为2PC、3PC、TCC、最终一致性等多种解决方案,这里我就简单说下RocketMQ是如何来解决分布式事务问题的,RocketMQ中有个Half Message半消息,指的是暂时不能被Consumer消费的消息,Producer已经把消息成功发送到了Broker端并且存储起来,被Broker端标记此类消息为暂不能投递的状态,这种消息被称为半消息,需要Producer的二次确认之后,才会发送给Consumer去消费

如果由于网络问题,或者生产者出现问题,导致Producer端一直没有对半消息Half Message进行二次确认,Broker会定时扫描长期处于半消息状态的消息,并且会主动去询问Producer这个消息的最终状态,是Commit还是Rollback,被称为消息回查。

5、过滤消息

这个其实比较简单,在RocketMQ中分为Broker端的消息过滤和Consumer端的消息过滤,其实也很好理解,一种就是在Broker层面根据Consumer的设置进行过滤,优点就是减少了对于Consumer无用消息的网络传输。缺点是增加了Broker的负担,实现起来相对比较负责。再一种就是根据Consumer进行过滤,这种方式可以完全自定义,缺点就是会有很多的无用的消息传递到Consumer端

6、定时消息和回溯消息

RocketMQ还支持定时消息和回溯消息,定时消息指的就是消息发送到Broker之后,不会被Consumer立即消费,而是要等到特定的时间点或者特定的时间才可以被消费。RocketMQ支持定时消息,但是不支持任意的时间精度,只支持特定的时间精度。

回溯消息指的是Consumer已经消费成功的消息需要二次消费,Broker在向Consumer投递成功消息之后,消息仍然需要保留,并且可能按照一定的时间维度进行消费。比如由于Consumer出现问题,可能需要重新消费几小时之前的数据,RocketMQ支持按照时间来回溯消息,时间维度精确到毫秒,可以向前或者向后。

求赞

好了,以上就是全部内容了,简单的把RocketMQ过了一遍,大家也对其有所了解了,能看到这里的肯定能学到一些东西

我希望有一天能够靠写字养活自己,现在还在磨练,这个时间可能会有很多年,感谢你们做我最初的读者和传播者。请大家相信,只要给我一份爱,我终究会还你们一页情的。

再次感谢大家能够读到这里,我后面会持续的更新技术文章以及一些记录生活的灵魂文章,如果觉得不错的,觉得【小仙】有点东西的话,求点赞、关注、分享三连

哦,对了!后续的更新文章我都会及时放到这里,欢迎大家点击观看,都是干货文章啊,建议收藏,以后随时翻阅查看

github.com/DayuMM2021/…