以下均为个人学习记录,如有误可提出建议 ——我有可能不同意你的说法,但我誓死捍卫你说话的权利!
第三节,一定要结合源码来看,不然无法看懂
RocketMQ深入浅出
一、结构介绍
broker:处理消息的主要服务
client:存放Product、与Consumer
namesrv:整个架构的注册中心
store:消息存储模块
二、核心
2.1 rocket 持久化过程
2.2 存储介质
2.1 文件系统[性能优秀很多](rocketMQ/kafkaMQ/rabbitMQ)
2.2 数据库[消息量太大将出现性能瓶颈](activeMQ)
2.3 rocket保证发送与接收的性能
2.4 使用SSD写的速度特别快、存储消息同时时候使用了:[顺序写]。
2.5 读取数据特别快:零拷贝,此技术使用要求文件大小在:1.5~2G,所以rocketMQ默认CommitLog文件大小1G,图示:
3.3 rocket存储结构:
3.1 commitLog:存放消息的文件。
3.2 consumeQueue:是查询消息的索引。 如果ConsumeQueue 丢失,可CommitLog中还原出来。一个topic有多个队列,每个队列分别对应一个consumeQueue: topic > Quque > consumeQueue。consumeQueue会被加入到内存中,加快读取的速度。
3.3 index:其他的查询消息的索引(key,时间,区间)。
3.4 总结:消息存放在commitLog中,然后每个commitLog文件默认为1G不超过2G(为了使用零拷贝技术),当消费者需要查询数据时,先到ConsumerQueue中查询索引,然后根据索引到CommitLog中查询数据。同时也可以使用其他的查询方法(到index文件中,根据key、时间、区间)获取索引后,再到CommitLog中获取数据。
2.4 刷盘机制
- 同步刷盘:写入内存,在写入磁盘中后就告知消息生产者消息存放完毕,然后再重新唤醒阻塞线程。
- 异步刷盘:写入内存后就告知生产者消息存入完成,然后再另外起一个线程存放到磁盘。
2.5 高可用性
- nameServer(无状态,新加一个nameServer不需要重启broker):集群搭建;
- broker:集群搭建,broker-master:写。broker-salve:读;
- produce(保证发送一定成功,需要搭建双主双从的模式):可发送给两个broker,如果其中一个挂了就给另外一个发送;
- consumer(天生的自动切换的机制):一般是在master读,如果繁忙则到salve中读;
- broker主从复制
5.1 同步复制:给master写数据,同时给salve写入成功后再返回给生产者消息。数据量很大则写入延迟,可保证数据的备份。
5.2 异步复制:给master写数据,就给生产者发送消息,然后再去同步数据到salve节点中。有可能造成数据丢失。
5.3 在broker.conf中配置:
#- ASYNC_MASTER 异步复制 Master
#- SYNC_MASTER 同步双写 Master
#- SLAVE 从节点配置
#- brokerRole=ASYNC_MASTER - 建议刷盘机制配置为异步。
平时项目的配置建议:异步刷盘+同步复制。
异步刷盘保证吞吐量,同步复制保证消息不丢失。
2.6 负载均衡
- 生产者负载均衡:基于topic路由实现,一个topic关联多个broker,放消息是按照queue排列顺序排放,rocketMQ内部已实现,无需配置。
- 消费者负载均衡:三个消费者已经把消息消费完,如果在多一个消费者,此消费者则不会工作。
集群模式:启动多个相同消费者,rocketMQ会自动分配。
广播模式:非集群模式,每个消费者都要消费一次消息,无需做负载均衡。
2.7 消息重试
2.7.1 顺序消费:
严格得顺序保障,前面消费者未消费完成,后面的消费者无法消费数据(产生阻塞,后面消费者将无法消费)。
2.7.1.1 全局顺序:
对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。
2.7.1.2 分区顺序:
对于指定的一个 Topic,所有消息根据 sharding key 进行区块分区。 同一个分区内的消息按照严格的 FIFO 顺序进行发布和消费。
- 生产者生产顺序消息:发送消息就要确保发送的消息是有序的,需要配合broker来处理,当返回ack时才发送下一条消息。
- Broker保存顺序消息:当生产者发送消息后broker存放的消息就是有序的,需要确保每一条保存成功后才返回ack到生产者。
- 消费者顺序消费消息:多线程情况下需要多执行消费的步骤加锁,消费时只能是一个线程进行消费。
2.7.2 无序消费
- 集群模式有效,广播模式无效。消费失败默认允许消费16次,每次间隔时间(默认时间间隔)不同,重复16次总的时间为4小时46分钟,如果大于16次,每次消费失败重试间隔时间为2小时。
- 如果消息消费4小时46分钟还未消费成功,将进入死信队列。消费后返回为空、异常则表示消费失败可重新消费。
- 消费失败了但不想重试,则在java代码中用try-catch块返回message成功的状态,则不会再重试了。
- 可获得消息重试得次数,再message对象中。可调整重试得次数,再启动时指定次数,一个消费组设置后,其他组内的消费者同理会适用此配置。
2.8 死信队列
- 产生:消费16次还未消费成功,则进入死信队列。
- 期限:此消息保存时间为3天。面向同一个消费者组。
- 来源:非某一个消费者,而是一个消费者组。
- 查看:死信队列可包含多个topic的消息。可通过控制台查看死信队列。
- 处理:可在控制台进行消息重发,可指定消费者消费指定的死信消息。
2.9 消息幂等
消费者消费同一条消息,不管消费多少次结果都是一样。消费方接收了多次重复的消息
2.9.1 产生的场景
- 生产者发送重复的消息。发送消失时,由于网络闪短,无法给生产者发送接收成功的反应,导致生产者以为发送失败。
- 消息者重复接收。消费者处理消失时由于网络闪断,未返回消费成功的反应给rocket,则以为消费失败。
- 负载均衡,消费者组中实例发生重新排序的变化,导致相同消费者消费了相同的消息。
2.9.2 解决方案
只能解决如果出现重复消息,如何保证幂等性,根据消息id来保证消息唯一(rocket无法保证消息id的唯一性),最好使用业务标识来处理,对消息设置业务key,消费方拿到业务key来判断消息是否被处理,在消费方把消费的消息存放起来,在消费之前查询一下此消息的key是否存在,如果存在则直接舍弃掉。
2.9 RocketMQ涉及的相关文件
- commitLog:存放消息的文件,默认为1G不得超过2G;
- borker-b-s.config:broker启动的时候所需要的配置;
- consumequeue:消费者在获取commitLog的时候所使用的索引;
- index:使用key、时间、区间查询时候所需要的索引;
- checkpoint:commitLog写入的位置
- abort:broker启动状态的文件,当broker停止时会被删掉
三、Rocket各模块执行过程(需要结合源码)
3.1 启动NameSrv(维护topic、broker数据)
- NameSrvController,需要使用NameSrvConfig、NettyServerConfig。
- 初始化NameSrvController,启动NameSrvController,其中会设置一个定时器,每隔5秒检测活跃的broker。当broker最后活跃时间加上2分钟,还小于当前时间就认定此broker不活跃,则关闭broker链接,同时在broker活跃表中删除此broker。
- 启动之前做一个jvm钩子方法,当退出rocketMq时执行的一个call方法。
- DefaultRequestProcessor会通过RequestProcess处理各个客户端发送过来的注册请求,在RouteInfoManager中的registerBroker方法中注册broker的信息。在注册的时候会判断broker的版本,大于V3_0_11可以设置broker的过滤服务(filterServerList)。
3.2 启动broker
- 创建BrokerController,需要创建BrokerConfig,与NettyServerConfig。在创建BrokerController,最后会有一个钩子方法,用户在broker退出时清除使用的资源。
- 获取BrokerController对象后再调用start启动方法。
- 添加broker:启动后会向namesrv发送当前broker的信息,同时会创建一个定时线程,每隔30秒去注册自己的信息。
- 删除broker:发送请求删除broker,定时任务删除broker(定时任务是在namesrv启动的时候开启的扫描活跃broker的定时器处理),当broker关闭时也会删除broker。
3.3 Producer(客户端)
需要研究的类:
MQAdmin(接口)、MQProducer(接口)、ClientConfig(类)、DefaultMQProducer
3.3.1 Producer启动的流程:
在DefaultMQProducerImpl中的start启动,首先是检查配置是否满足启动,然后再设置Producer名称,然后再获取produce创建的工厂,再通过工厂注册一个Producer,然后再放置到MQProducerInner里面。
3.3.2 消息发送的流程:
在DefaultMQProducerImpl实现类中执行发送消息。
- 对消息进行校验:
校验topic、校验消息长度是否超过最大长度4M - 发送消息查找路由表(知道当前消失往那个broker发):
首先去TopicPublishInfoTable(本地缓存)中获取topic数据,如果无法获取到就发送请求到NameSrv中获取,获取完毕后需要判断是否与本地的路由表中数据发生了变化,如果发生变化就更新本地缓存。 - 选择队列:
首先会根据传递的topic查询Queue,在选择Queue时有两种情况,一是开启了获取broker发送延迟故障队列的机制,如果开启了后就通过for循环 先依次获取topic中的队列,在获取时会判断是否是之前存放在LatencyFaultTolerance中发送失败的队列,是则再次查找,不是就直接返回。如果循环了所有的topic中的Queue都没有找到一个可直接发送正常的Queue,就在失败队列的集合中找一个比较好的Queue返回。二是未开启延迟故障队列的机制,就直接通过topic获取Queue不管是否未之前失败的队列,获取流程是,先判断是否为第一次发送消息,如果是则直接根据下标返回,如果不是就循环topic的所有队列,拿取非上次发送消息的队列,并返回。 - 发送消息:
1.首先拿取一个broker地址;
2.然后在生成一个消息的id;
3.判断消息是否需要压缩,如果消息大于了4k就标识需要对消息进行压缩;
4.判断在消息检查的时候是否添加检查钩子函数,如果添加就先执行;
5.判断在发送消息前是否添加发送消息钩子函数,如果添加就执行;
6.封装request请求包;
7.根据不同的发送模式发送消息(oneway与同步发送消息是相同的方法),异步发送需要回调对象,同步发送不需要回调对象;
8.然后再通过MQClientAPIImpl发送消息; - 批量发送消息的流程(条件:同一个主题、不能超过最大消息长度;优点:减少网络链接次数,提交效率;):
首先将多个消息封装到一个messageBatch中,然后循环MessageBatch中的每个消息再检查消息,并给每条消息设置一个消息id与topic,然后再对整个messageBatch进行编码,编码完成后再对整个messageBatch设置一个topic。然后再执行单条消息发送的流程。
3.4 消息存储
为了消息存放更加快速,先将消息添加到内存当中,然后根据选择的模式来选择是否要即刻存放到硬盘
3.4.1 主要实现技术:内存映射技术提高IO的性能;
3.4.2 主要实现类:
- MessageStore:消息存储的接口;
- DefaultMessageStore:消息存储的主要实现;
- MappedFile:消息查询、刷盘的主要实现,两种实例化,开启堆外内存(先存放到堆外内存在提交到磁盘)、未开启堆外内存就直接实例化到磁盘;
- MappedFileQueue:消息查询、刷盘类的封装;
- TransientStorePool:堆外内存缓存池;
3.4.3 存放消息主要使用的类: ByteBuffer(缓冲区)、刷盘;
3.4.4 存放消息的流程
首先将消息存放到CommitLog,然后再将消息索引异步存放到ConsumerQueue与Index:
存放消息时首先会在asyncPutMessage方法中检查PutMessageStatus状态是否为启用、检查是否为从节点、检查消息是否超长,然后主要是在CommitLog中的doAppend方法中执行保存。首先会生成一个写入的位置,然后会生成消息的id,会判断消息的flag值看是否有需要特殊处理的事务,会在写入前判断是否还有足够的空间(消息的长度 > 消息的长度-减去插入的位置)如果超过长度就会清空 前面插入在当前缓冲区的数据,然后生成返回信息返回标识为END_OF_FILE退出doAppend方法。退出后会进入一个switch,重新再进入如果写入OK就会去执行刷盘操作,如果返回标识为END_OF_FILE就会再次进入appendMessage执行数据写入。当缓冲区写入完成后,就会执行刷盘的代码,刷盘时可设置同步刷盘、与异步刷盘。
3.4.5 消息存储后对消息进行分发
分发到ConsumerQueue与Index
- 主要实现类:ReputMessageService(这个类也是一个线程)
- 执行步骤:DefaultMessageStore在调用start启动的时候,同时也会执行ReputMessageService中的start方法,此方法中有一个doReput方法主要对消息做分发的方法,在此方法中首先会使用getData方法查询需要分发的消息,然后通过for循环遍历所有消息,再将消息通过doDispatch方法进行分发,因为doDispatch方法主要是在CommitLogDispatcher接口中定义,所以此方法的主要实现有CommitLogDispatcherBuildConsumeQueue类的dispatch方法与CommitLogDispatcherBuildIndex类的dispatch方法,因此分发消失时会分别分发到ConsumerQueue与Index中。
3.4.6 消息恢复
- 产生原因:在存放消息到commitLog后,异步更新ConsumerQueue与Index文件时失败,导致消息不一致。如果出现此情况,此条消息将永远无法被消费者消费。
- 消息恢复的入口:在DefaultMessageStore类中的load方法,首先检查broker是否正常退出(判断依据:判断依据是rocket启动项目的时候会在data/store文件夹中创建一个abort文件,如果关闭broker时此文件被删除了表示broker正常退出,存在则表示异常退出。),然后加载CommitLog文件,再加载ConsumerQueue文件,然后获取文件检查点,在此文件检测点中存放了上一次文件转发的位置,拿到位置后就开始执行恢复,执行恢复时会根据不同的退出情况执行不同的恢复策略(异常恢复/正常恢复),会进入recover方法中,在此方法中会:正常恢复会进入CommitLog中的recoverNormally方法,先文件映射队列(MeppedFileQueue)中获取需要的MappedFile,然后再拿取上次处理的位置,拿到位置后查询出需要恢复的消息与消息的长度,拿到后会先判断此消息是否已同步,如果同步就重新再拿取,然后再判断MappedFile是否拿取到最后一条消息,如果拿取到最后一条消息,就给index加1,然后在MappedFileQueue中获取下一个映射文件,如果恢复到队列的最后一个文件,那么就不在继续恢复。异常恢复:和上面的流程相差不大,主要就是恢复最后一个文件。
3.4.7 刷盘机制
在CommitLog类中,有一个SubmitFlushRequest方法,在方法中可分为,同步刷盘与异步刷盘:同步刷盘会先获取有一个GroupCommitService
服务,然后再获取一个刷盘请求,创建请求时会设置一个超时时间,默认是5秒钟(请求会阻塞5秒),然后将请求写入到GroupCommitService服务中,
在执行写入服务后,会设置倒计时锁的时间,从而唤醒执行数据写入的线程去执行run,在run方法中会先将前面存入到写链表(requestWrite)集合的数
据放到读链表(requestRead),然后再通过doCommit方法执行刷盘。执行刷盘前会判断当前消息的位置是否大于刷盘的位置,判断小于下一输盘位置,会
执行两次刷盘,防止有跨文件的消息。如果大于则刷盘失败。消息会先存放到堆外内存然后再到屋里内存。
异步刷盘:会先判断 是否开启异步刷盘、不可为从节点、刷新类型是否为FlushDiskType.ASYNC_FLUSH
四、使用场景
- 不同系统间业务调用。
- 分布式事务【消息队列 + 定时任务 + 本地事件表】,可靠性构建。