由于篇幅原因,本次的源码分析只限于Producer侧的发送消息的核心逻辑,我会通过流程图、代码注释、文字讲解的方式来对源码进行解释,后续应该会专门开几篇文章来做源码分析。

这篇博客聊聊关于RocketMQ相关的东西,主要聊的点有RocketMQ的功能使用、RocketMQ的底层运行原理和部分核心逻辑的源码分析。至于我们为什么要用MQ、使用MQ能够为我们带来哪些好处、MQ在社区有哪些实现、社区的各个MQ的优劣对比等等,我在之前的文章《消息队列杂谈》已经聊过了,如果需要了解的话可以回过头去看看。

基础概念

Broker

首先我们要知道,使用RocketMQ时我们经历了什么。那就是生产者发送一条消息给RocketMQ,RocketMQ拿到这条消息之后将其持久化存储起来,然后消费者去找MQ消费这条消息。


RocketMQ操作

上图中,RocketMQ被标识为了一个单点,但事实上肯定不是如此,对于可以随时横向扩展的服务来说,生产者向MQ生产消息的数量也会随之而变化,所以一个合格成熟的MQ必然是要能够处理这种情况的;而且MQ自身需要做到高可用,否则一旦这个单点宕机,那所有存储在MQ中的消息就全部丢失且无法找回了。

所以在实际的生产环境中,肯定是会部署一个MQ的集群。而在RocketMQ中,这个“实例”有个专属名词,叫做Broker。并且,每个Broker都会部署一个Slave Broker,Master Broker会定时的向Slave Broker同步数据,形成一个Broker的主从架构

那么问题来了,在微服务的架构中,部署的服务也存在多实例部署的情况,服务之间相互调用是通过注册中心来获取对应服务的实例列表的。

拿Spring Cloud举例,服务通过Eureka注册中心获取到某个服务的全部实例,然后交给Ribbon,Ribbon联动Eureka,从Eureka处获取到服务实例的列表,然后通过负载均衡算法选出一个实例,最后发起请求。

同理,此时MQ中存在多个Broker实例,那生产者如何得知MQ集群中有多少Broker实例呢?自己应该连接哪个实例?

首先我们直接排除在代码里Hard Code,具体原因我觉得应该不用再赘述了。RocketMQ是如何解决这个问题呢?这就是接下来我们要介绍的NameServer了。

NameServer

NameServer可以被简单的理解为上一小节中提到的注册中心,所有的Broker的在启动的时候都会向NameServer进行注册,将自己的信息上报。这些信息除了Broker的IP、端口相关数据,还有RocketMQ集群的路由信息,路由信息后面再聊。


RocketMQ操作

有了NameServer,客户端启动之后会和NameServer交互,获取到当前RocketMQ集群中所有的Broker信息、路由信息。这样一来,生产者就知道自己需要连接的Broker信息了,就可以进行消息投递。

那么问题来了,如果在运行过程中,如果某个Broker突然宕机,NameServer会如何处理?

这需要提到RocketMQ的这续约机制故障感知机制。Broker在完成向NameServer的注册之后,会每隔30秒向NameServer发送心跳进行续约;如果NameServer感知到了某个Broker超过了120秒都没有发送心跳,则会认为这个Broker不可用,将其从自己维护的信息中移除。

这套机制,和Spring Cloud中的Eureka的实现如出一辙。Eureka中的Service在启动之后也会向Eureka注册自己,这样一来其他的服务就可以向该服务发起请求,交换数据。Service每隔30秒会向Eureka发送心跳续约,如果某个Service超过了90秒没有发送心跳,Eureka就会认为该服务宕机,将其从Eureka维护的注册表中移除。

上面图中我聊到了多实例部署,这个多实例部署和微服务中的多实例部署还不太一样,微服务中,所有的服务都是无状态的,可以横向的扩展,而在RocketMQ中,每个Broker所存的数据可能都不一样。

我们来看一下RocketMQ的简单使用。

Message msg = new Message(
  "TopicTest",
  "TagA",
  ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
);
SendResult sendResult = producer.send(msg);

可以看到,Message的第一个参数,为当前这条消息指定了一个Topic,那Topic又是什么呢?

Topic

Topic是对发送到RocketMQ中的消息的逻辑分类,例如我们的订单系统、积分系统、仓储系统都会用到这个MQ,为了对其进行区分,我们就可以为不同的系统建立不同的Topic。

那为什么说是逻辑分区呢?因为RocketMQ在真实存储中,并不是一个Broker就存储一个Topic的数据,道理很简单,如果当前这个Broker宕机,甚至极端情况磁盘坏了,那这个Topic的数据就会永久丢失。

所以在真实存储中,消息是分布式的存储在多个Broker上的,这这些分散在多个Broker上的存储介质叫MessageQueue,如果你熟悉Kafka的底层原理,就知道这个跟Kafka中的Partition是同类的实现。


Message Queue存储

通过上图可以看出,同一个Topic的数据,被分成了好几份,分别存储在不同的Broker上,那RocketMQ为什么要这么实现?

首先,一个Topic中如果只有一个Queue,那么消费者在消费时的速度必然受到影响;而如果一个Topic有很多个Queue,那么Consumer就可以将消费操作同时进行,从而扛住更多的并发。

除此之外,单台机器的资源是有限的。一个Topic的消息量可能会非常之巨大,一台机器的磁盘很快就会被塞满。所以RocketMQ将一个Topic的数据分摊给了多台机器,进行分散存储。其本质上就是一个数据分片存储的一种机制。

所以我们知道了,发送到某个Topic的数据是分布式的存储在多个Broker中的MessageQueue上的。

Broker消息存储原理

那Producer发送到Broker中的消息,到底是以什么方式存储的呢?答案是Commit Log,Broker收到消息,会将该消息采用顺序写入的方式,追加到磁盘上的Commit Log文件中,每个Commit Log大小为1G,如果写满了1G则会新建一个Commit Log继续写,Commit Log文件的特点是顺序写、随机读。


Topic详情

这就是最底层的存储的方式,那么问题来了,Consumer来取消息的时候,Broker是如何从这一堆的Commit Log中找到相应的数据呢?众所周知,一提到磁盘的I/O操作,就会联想到耗时这两个字,而RocketMQ的一大特点就是高吞吐,看似很矛盾,RocketMQ是如何做的呢?

答案是ConsumeQueue,Broker在写入Commit Log的同时,还会将当前这条消息在Commit Log中的Offset、消息的Size和对应的Tag的Hash写入到ConsumeQueue文件中。每个Message Queue会有相对应的ConsumeQueue文件存储在磁盘上。

和Commit Log一样,一个ConsumeQueue包含了30W条消息,每条消息的大小为20字节,所以每个ConsumeQueue文件的大小约为5.72M;当其写满了之后,会再新建一个ConsumeQueue文件继续写入。

ConsumeQueue是一种逻辑队列,更是一种索引,让Consumer来消费的时候可以快速的从磁盘文件中定位到这条消息。

看到这你可能会想,上面提到的Tag又是个什么东西?

Tag

Tag,标签,用于对同一个Topic内的消息进行分类,为什么还需要对Topic进行消息类型划分呢?

举一个极端的例子,某一个新的服务,需要去消费订单系统的MQ,但是由于业务的特殊性,只需要去消费商品类型为数码产品的订单消息,如果没有Tag,那么该Consumer就会去做判断,该订单消息是否是数码产品类,如果不是,则丢弃,如果是则进行消费。

这样一来,Consumer侧就执行了大量的无用功。引入了Tag之后,Producer在生产消息的时候会给订单打上Tag,Consumer进行消费的时候,可以配置只消费指定的Tag的消息。这样一来就不需要Consumer自己去做这个事情了,RocketMQ会帮我们实现这个过滤。

那其过滤的原理是什么?首先在Broker侧是通过消息中保存的Tag的Hash值进行过滤,然后Consumer侧在去拉取消息的时候还需要再过滤一次。

为什么在Broker过滤了,还需要在Consumer侧再过滤一次?因为Hash冲突,不同的Tag经过Hash算法之后可能会得到一样的值,所以Consumer侧在拉取消息的时候会通过字符串进行二次过滤。

Producer发送消息源码分析

流程总览

首先给出整个发送消息的大致流程,先熟悉这个流程看源码,会更加的清晰一点。


总体流程

初始化Prodcuer

还是按照下面这个例子出发。


producer使用样例

首先我们会初始化一个DefaultMQProducer,RocketMQ会给这个Producer一个默认的实现DefaultMQProducerImpl。然后producer.start()会启动一个线程池。

合法性校验

接下来就是比较核心的producer.send(msg)了,首先RocketMQ会调用checkMessage来检测发送的消息是否合法。


send消息

这些检测包含了待发送的消息是否为空,Topic是否为空、Topic是否包含了非法的字符串、Topic的长度是否超过了最大限制127,然后会去检查Body是否符合发送要求,例如msg的Body是否为空、msg的Body是否超过了最大的限制等等,这里消息的Body最大不能超过4M。


检查消息合法性源码

调用发送消息

对于msg的Topic,RocketMQ会用NameSpace将其包装一层,然后就会调用DefaultMQProducerImpl中的sendDefaultImpl默认实现,发送消息给Broker,默认的发送消息Timeout是3秒。


发送消息默认实现

发送消息中,MQ会再次调用checkMessage对消息的合法性再次进行检查,然后就会去尝试获取Topic的详细信息。

所有的Topic的信息都会存在一个叫topicPublishInfoTable的 ConcurrentHashMap中,这个Map中Key就是Topic的字符串,而Value则是TopicPublishInfo

这个TopicPublishInfo中就包含了之前在基础概念中提到的,从Broker中获取到的相应的元数据,其中就包含了关键的MessageQueue和集群元数据,其基础的结构如下。


Topic详情

messageQueueList包含了该Topic下的所有的MessageQueue,每个MessageQueue的所属Topic,每个MessageQueue所在的Broker的名称以及专属的queueId。

topicRouteData包含了该Topic下的所有的Queue、Broker相关的数据。

获取Topic详细数据

在最终发送消息前,需要获取到Topic的详情,例如像Broker地址这样的数据,Producer中是通过tryToFindTopicPublishInfo方法获取的,详细的注释我已经写在了下图中。


获取topic详情

对于首次使用的Topic,在上面的Map肯定是不存在的。所以RocketMQ会将其加入到Map中去,并且调用方法updateTopicRouteInfoFromNameServer从NameServer处获取该Topic的元数据,将其一并写入Map。初次之外,还会将路由信息、Broker的详细信息分别放入topicRouteTablebrokerAddrTable中,这两个都是Producer维护在内存中的ConcurrentHashMap。

获取到了Topic的详细信息之后,接下来会确认一个发送的重试次数timesTotal,假设timesTotal为N,那么发送消息如果失败就会重试N次。不过当且仅当发送失败的时候才会进行重试,其余的case都不会,例如超时、或者没有选择到合适的MessageQueue。

这个重试的次数timesTotal受到参数communicationMode的影响;CommunicationMode有三个值,分别是SYNCASYNCONEWAY。RocketMQ默认的实现中,选择了SYNC同步。


计算重试次数

通过代码我们可以看到,如果是communicationModeSYNC的话,timesTotal的值为1+retryTimesWhenSendFailed,而retryTimesWhenSendFailed的值默认为2,代表在消息发送失败之后的重试次数。

这样一来,如果我们选择了SYNC的方式,Producer在发送消息的时候默认的重试次数就为3。不过当且仅当发送失败的时候才会进行重试,其余的case都不会。

MessageQueue选择机制

我们之前聊过,一个Topic的数据是分片存储在一个或者多个Broker上的,底层的存储介质为MessageQueue,之前的图中,我们没有给出Producer是如何选择具体发送到哪个MessageQueue,这里我们通过源码来看一下。

Producer中是通过selectOneMessageQueue来进行的Message Queue选择,该方法通过Topic的详细元数据和上次选择的MessageQueue所在的Broker,来决定下一个的选择。

核心的选择逻辑

核心的选择逻辑是什么呢?用大白话来说,就是选出一个index,然后将其和当前Topic的MessageQueue数量取模。这个index在首次选择的时候,肯定是没有的, RocketMQ会搞一个随机数出来。然后在该值的基础上+1,因为为了通用,在外层看来,这个index上次已经用过了,所以每次获取你都直接帮我+1就好了。


核心的选择机制

上图就是MessageQueue最核心的、最底层的原则机制了。但是由于实际的业务情况十分复杂, RocketMQ在实现中还额外的做了很多的事情。

发送故障延迟下的选择逻辑

在实际的选择过程中,会判断当前是否启用了发送延迟故障,这个由变量sendLatencyFaultEnable的值决定,其默认值是false,也就是默认是不开启的,从代码里我暂时没找到其开启的位置。

不过我们可以聊聊开启之后,会发生什么。它同样会开启for循环,次数为MessageQueue的数量,计算拿到确定的Queue之后,会通过内存的一张表faultItemTable去判断当前这个Broker是否可用,该表是每次发送消息的时候都会去更新它。

如果当前没有可用的Broker,则会触发其兜底的逻辑,再选择一个MessageQueue出来。


选择queue的源码

常规的选择逻辑

如果当前发送故障延迟没有启用,则会走常规逻辑,同样的会去for循环计算,循环中取到了MessageQueue之后会去判断是否和上次选择的MessageQueue属于同一个Broker,如果是同一个Broker,则会重新选择,直到选择到不属于同一个Broker的MessageQueue,或者直到循环结束。这也是为了将消息均匀的分发存储,防止数据倾斜。


常规逻辑下的选择逻辑

消息发送

最后就会调用Netty相关的组件,将消息发送出去了。

EOF

关于RocketMQ中的一些基础的概念,和RocketMQ的Producer发送消息的源码就先分析到这里,后续看缘分再分享其他部分的源码吧。

好了以上就是本篇博客的全部内容了,如果你觉得这篇文章对你有帮助,还麻烦点个赞关个注分个享留个言

欢迎微信搜索关注【SH的全栈笔记】,查看更多相关文章

RocketMQ基础概念剖析,并分析一下Producer的底层源码的更多相关文章

  1. List-ArrayList集合基础增强底层源码分析

    List集合基础增强底层源码分析 作者:Stanley 罗昊 [转载请注明出处和署名,谢谢!] 集合分为三个系列,分别为:List.set.map List系列 特点:元素有序可重复 有序指的是元素的 ...

  2. List-LinkedList、set集合基础增强底层源码分析

    List-LinkedList 作者 : Stanley 罗昊 [转载请注明出处和署名,谢谢!] 继上一章继续讲解,上章内容: List-ArreyLlist集合基础增强底层源码分析:https:// ...

  3. 2018.11.20 Struts2中对结果处理方式分析&struts2内置的方式底层源码剖析

    介绍一下struts2内置帮我们封装好的处理结果方式也就是底层源码分析 这是我们的jar包里面找的位置目录 打开往下拉看到result-type节点 name那一列就是我们的type类型取值 上一篇博 ...

  4. 鸿蒙源码分析系列(总目录) | 百万汉字注解 百篇博客分析 | 深入挖透OpenHarmony源码 | v8.23

    百篇博客系列篇.本篇为: v08.xx 鸿蒙内核源码分析(总目录) | 百万汉字注解 百篇博客分析 | 51.c.h .o 百篇博客.往期回顾 在给OpenHarmony内核源码加注过程中,整理出以下 ...

  5. 全方位深度剖析PHP7底层源码(已完结)

    第1章 课程介绍本章主要介绍课程要讲的知识点,以及课程要求等. 第2章 PHP7的新特性本章主要介绍PHP7的新特性,做基准测试,与PHP5对比验证PHP7的性能提升程度,引出对PHP7源码学习的必要 ...

  6. BAT资深工程师 由浅入深分析 Tp5&Tp6底层源码 - 分享

    BAT资深工程师由浅入深分析Tp5&Tp6底层源码 第1章 课程简介 本章主要让大家知道本套课程的主线, 导学内容,如何学习源码等,看完本章要让小伙伴觉得这个是必须要掌握的,并且对加薪有很大的 ...

  7. Tomcat源码分析一:编译Tomcat源码

    Tomcat源码分析一:编译Tomcat源码 1 内容介绍 在之前的<Servlet与Tomcat运行示例>一文中,给大家带来如何在Tomcat中部署Servlet应用的相关步骤,本文将就 ...

  8. BAT资深工程师由浅入深分析Tp5&Tp6底层源码☆

    第1章 课程简介 本章主要让大家知道本套课程的主线, 导学内容,如何学习源码等,看完本章要让小伙伴觉得这个是必须要掌握的,并且对加薪有很大的帮助. 第2章 [TP5灵魂]自动加载Loader 深度分析 ...

  9. Android源码分析(十一)-----Android源码中如何引用aar文件

    一:aar文件如何引用 系统Settings中引用bidehelper-1.1.12.aar 文件为例 源码地址:packages/apps/Settings/Android.mk LOCAL_PAT ...

随机推荐

  1. Git实践笔记(二)

    前一篇中说完了本地仓库的一些操作,本篇将继续进行撤销修改和删除的操作,以及远端仓库的设置.提交.克隆. 撤销修改和删除 现在在test.txt文件里面增加一行,内容为add error msg,先通过 ...

  2. 使用Python调用SMTP服务自动发送Email

    需求背景 假设我们想设计一个定时任务,比如每天定时的用python来测试服务是否在正常运行,但是又不希望每天登录到系统后台去查看服务状态.这里我们就可以采取python的smtp模块进行任务结果广播, ...

  3. Codeforces Round #582 (Div. 3) G. Path Queries (并查集计数)

    题意:给你带边权的树,有\(m\)次询问,每次询问有多少点对\((u,v)\)之间简单路径上的最大边权不超过\(q_i\). 题解:真的想不到用最小生成树来写啊.... 我们对边权排序,然后再对询问的 ...

  4. Windows Server 2016 开启远程连接并延长过期时间

    按照下面文章配置,做完1.2步即可,其中协议号码填写 4954438 亲测有效! Server 2016默认远程桌面连接数是2个用户,如果多余两个用户进行远程桌面连接时,系统就会提示超过连接数,可以通 ...

  5. 2.Url重定向和重写

    虚拟地址===>真实地址映射(搜索引擎优化,抽象能力,防盗链) 之前:IIS重写模块规则,Apache mod_Rewrite.Nginx上的URL重写. 现在:通过中间件来实现. 重定向与重写 ...

  6. k8s二进制部署 - harbor安装

    harbor安装 # 目录说明: # /opt/src : 源码.文件下载目录 # /opt/release : 各个版本软件存放位置 # /opt/apps : 各个软件当前版本的软链接 [root ...

  7. k8s-0-集群

    Docker回顾 docker容器封装应用程序好处 内核在3.8以上,才能完整使用docker隔离功能(所有centos6不推荐用) Docker容器化封装应用程序缺点 容器编排工具有哪些 一: K8 ...

  8. Redis Cluster 分布式集群(下)

    Redis Cluster 搭建(工具) 环境准备 节点 IP 端口 节点① 172.16.1.121 6379,6380 节点② 172.16.1.122 6379,6380 节点③ 172.16. ...

  9. Linux命令之find命令中的-mtime参数

    有关find -mtime的参数解释 mtime参数的理解应该如下: -mtime n 按照文件的更改时间来找文件,n为整数. n表示文件更改时间距离为n天, -n表示文件更改时间距离在n天以内,+n ...

  10. 二进制安装kubernetes(四) kube-scheduler组件安装

    介绍资料转载地址:https://www.jianshu.com/p/c4c60ccda8d0 kube-scheduler在集群中的作用 kube-scheduler是以插件形式存在的组件,正因为以 ...