RocketMQ消息存储

5.1、常⽤的存储设计

MQ消息队列采⽤的存储⽅式主要有以下三种⽅式

1)分布式KV存储:这类MQ⼀般会采⽤诸如levelDB、RocksDB和Redis来作为消息持久化的⽅式,由于分布式缓存的读写能⼒要优于DB,所以在对消息的读写能⼒要求都不是⽐较⾼的情况下可以使⽤这种⽅案。

(2)⽂件系统:⽬前业界较为常⽤的⼏款产品(RocketMQ/Kafka/RabbitMQ)均采⽤的是消息刷盘⾄所部署虚拟机/物理机的⽂件系统来做持久化(刷盘⼀般可以分为异步刷盘和同步刷盘两种模式)。消息刷盘为消息存储提供了⼀种⾼效率、⾼可靠性和⾼性能的数据持久化⽅式。除⾮部署MQ机器本身或是本地磁盘挂了,否则⼀般是不会出现⽆法持久化的故障问题。

3)关系型数据库DB:Apache下开源的另外⼀款MQ—ActiveMQ(默认采⽤的KahaDB做消息存储)可选⽤JDBC的⽅式来做消息持久化,通过简单的xml配置信息即可实现JDBC消息存储。普通关系型数据库(如Mysql)在单表数据量达到千万级别时,其IO读写性能往往会出现瓶颈。并且,MQ消息队列通常要满⾜吞吐量⼤、消息堆积能⼒突出的要求,采⽤关系型数据库作为消息持久化的⽅案显然不是⼀个很好的选择。同时,由于依赖DB,⼀旦DB出现故障,MQ的消息则⽆法落盘存储,会导致线上业务数据丢失。

综合上所述从存储效率来说, ⽂件系统>分布式KV存储>关系型数据库DB,直接操作⽂件系统肯定是最快和最⾼效的。另外,从消息中间件的本身定义来考虑,应该尽量减少对于外部第三⽅中间件的依赖。⼀般来说依赖的外部系统越多,也会使得本身的设计越复杂,所以采⽤⽂件系统作为消息存储的⽅式,更贴近消息中间件本身的定义

5.2、整体架构

(1)消息⽣产与消息消费相互分离,Producer端发送消息最终写⼊的是CommitLog(消息存储的⽇志数据⽂件),Consumer端先从ConsumeQueue(消息逻辑队列)读取持久化消息的起始物理位置偏移量offset、⼤⼩size和消息Tag的HashCode值,随后再从CommitLog中进⾏读取待消费消息的实体内容;

(2)RocketMQ的CommitLog⽂件采⽤混合型存储(所有的Topic下的消息队列共⽤同⼀个CommitLog的⽇志数据⽂件)。通过建⽴类似索引⽂件—ConsumeQueue的⽅式,区分不同Topic下⾯的不同MessageQueue的消息,同时为消费消息起到⼀定的缓冲作⽤(只有ReputMessageService异步服务线程通过doDispatch异步⽣成了ConsumeQueue队列的元素后,Consumer端才能进⾏消费)。这样,只要消息写⼊,并刷盘⾄CommitLog⽂件后,消息就不会丢失,即使ConsumeQueue中的数据丢失,也可以通过CommitLog来恢复。

(3)发送消息时,⽣产者端的消息确实是顺序写⼊CommitLog;订阅消息时,消费者端也是顺序读取ConsumeQueue,然⽽根据其中的起始物理位置偏移量offset读取消息真实内容却是随机读取CommitLog。

RocketMQ的具体做法是,使⽤Broker端的后台服务线程—ReputMessageService不停地分发请求,并异步构建ConsumeQueue(逻辑消费队列)和IndexFile(索引⽂件)数据。然后,Consumer通过ConsumerQueue查找待消费的消息。其中,ConsumeQueue作为消费消息的索引,保存了指定Topic下的队列消息在CommitLog中的起始物理偏移量offset,消息⼤⼩size和消息Tag的HashCode值。⽽IndexFile只是为消息查询提供了⼀种通过key或时间区间来查询消息的⽅法。

从消息存储架构图中主要有下⾯三个跟消息存储相关的⽂件构成:

CommitLog :存储消息的元数据

ConsumerQueue :存储消息在 CommitLog 的索引

IndexFile :为了消息查询提供了⼀种通过 key 或时间区间来查询消息的⽅法,这种通过 IndexFile 来查找消息的⽅法不影响发送与消费消息的主流程。

5.2.1、commitLog

消息存储⽂件,所有主题的消息都存储在 Commitlog ,⽂件中单个⽂件⼤⼩默认 1G , ⽂件名⻓度为 20 位,左边补零,剩余为起始偏移量,⽐如 00000000000000000000 代表了第⼀个⽂件,起始偏移量为 0 ,⽂件⼤⼩为1 G

  • 1073741824。当第⼀个⽂件写满了,第⼆个⽂件为 00000000001073741824,起始偏移量为 1073741824,

以此类推。

1、commitLog结构

上⾯ Message 写⼊ 到磁盘的 Commitlog 采⽤顺序写的⽅式,相⽐较随机写的速度快很多保证了消息存储的速度。

5.2.2、ConsumeQueue

ConsumeQueue 是⼀组组⽂件,⽽⼀个 MessageQueue 对应其中⼀组 ConsumeQueue ⽂件,主要的作⽤是记录当前 MessageQueue 被哪些消费者组并且消费到了 Commitlog 中哪⼀条消息。

ConsumeQueue ⽬录下⾯是以 Topic 命名的⽂件夹,然后再下⼀级是以 MessageQueue 队列ID命名的⽂件夹,最后才是⼀个或多个 ConsumeQueue 、⽂件ConsumeQueue ⽂件存储了 CommitLog ⽂件中的偏移量 、 消息⻓度 、消息 Tag 的 hashcode 值

5.2.3、index

是以创建时的时间戳命名的,固定的单个 IndexFile ⽂件⼤⼩约为400M,⼀个Index ⽂件 可以保存 2000W 个索引,IndexFile 的底层存储设计为在⽂件系统中实现 HashMap 结构,所以RocketMQ 的索引⽂件其底层实现为 hash 索引

indexHead 的格式如下

*字段**解释*
beginTimestamp消息的最⼩存储时间
endTimestamp消息的最⼤存储时间
beginPhyOffset消息的最⼩偏移量(commitLog ⽂件中的偏移量)
endPhyOffset消息的最⼤偏移量(commitLog ⽂件中的偏移量)
hashSlotCounthash 槽个数
indexCountindex 条⽬当前已使⽤的个数

Index条目

*字段**解释*
hashcodekey 的hashcode
phyoffset消息的偏移量(commitLog⽂件中的偏移量)
timedif该消息存储时间与第⼀条消息的时间戳的差值,⼩于0该消息⽆效
pre index no该条⽬的前⼀条记录的Index索引,当hash冲突时,⽤来构建链表

Index ⽂件构成过程⽐较麻烦,其实就是类似于hashMap

  1. 将消息顺序放到 Index 条⽬中,将 11 放到 index=1 的位置(⽤index[1]表示),11%10=1,算出 hash 槽的位置是 1,存的值是 0(刚开始都是0,⽤hash[0]表示),将 index[1].preIndexNo=hash[0]=0,hash[0]=1

(1 为 index 数组下标)

  1. 将 34 放到 index[2],34%10=4,index[2].preIndexNo=hash[0]=0

  2. 将 21 放到 index[3],21%10=1,index[3].preIndexNo=hash[1]=1

从图中可以看出来,当发⽣ hash 冲突时 Index 条⽬的 preIndexNo 属性充当了链表的作⽤。查找的过程和 HashMap 基本类似,先定位到槽的位置,然后顺着链表找就⾏了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Martin_lr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值