源码分析基于 bitcask 的 rosedb 存储引擎内数据结构设计实现 (二)
上篇主要介绍了 rosedb 的 string 和 list 数据结构在 bitcask 存储模型下的实现原理。本文讲解 hash、set、zset (sorted set) 的实现原理。
golang bitcask rosedb 存储引擎实现原理系列的文章地址 (更新中)
https://github.com/rfyiamcool/notes#golang-bitcask-rosedb
hash 结构的处理流程
hash 写数据的实现
HSet
用来实现插入和更新 hash 字典,构建一个 entry 结构对象,然后该对象写到 logfile 里,entry 的 key 是由 key 和 field 编码生成的,目的在于启动恢复时通过 entry 的 key 值还原出 hash key 和 filed 两个字段。 最后在 radixTree 索引里添加 member 和 valuePos 的映射。
hash 读数据的实现
HGet
用来获取 hash 结构里 field 的 value,其内部的流程是这样,先获取 hash key 对应的 radixtree 索引,radixtree 索引上的 key 是 hash 的 field 字段,所以先从索引获取 field 对应的 valuePos,继而再从 logfile 里获取指定偏移的数据。
hash 删除 field 数据的实现
HDel
用来删除 hash 结构里的数据,删除流程就两步,追加写一条带 delete 标记的数据到 logfile 日志文件里,然后在索引里剔除该 field。
hash 进行扫描的实现
HScan
用来实现 hash 数据的扫描,但 rosedb 实现的 hcan 跟 redis hash hscan 不同,redis hscan 是有实现 cursor 游标。
set 结构的处理流程
set 集合写数据的实现
SAdd
用来把 member 成员插入到 set 集合里,其实现流程如下。
- 获取 set key 关联的 radixtree 索引,为空则实例化一个 radixtree 索引对象 ;
- 遍历需要写入的 members 列表 ;
- 计算 member 成员的哈希码,重置该 hash 计算器, 整个 set 集合共用一个 hash 计算对象,入口方法有锁保证其线程安全 ;
- 构建 entry 对象,并写到忽略 logfile 日志里 ;
- 构建一个索引的 entry 对象,key 使用 hashcode, 并把该 entry 更新到 radixtree 索引里。
其实 set 集合可以在 rosedb 的 hash 基础上封装下即可。相比 hash 字典来说,rosedb set 会 member 成员计算了 hash 值,radixTree 索引内部的 key 使用 hash 值, value 则还是 valuePos 文件偏移信息,有趣的是写到 logfile 里的 entry 格式有些奇怪,key 为 set 集合的 key,而 value 为 member 成员对象。rosedb 其他的数据结构的 key 有做处理的,要么跟 seq 一起编码,要么跟 field 一起编码。 这要求 rosedb 在启动阶段时对 set 结构的 logfile 做特殊处理,区别于其他结构。
set 和 zset 都使用 murmru hash 算法计算哈希值,其目的在于节省 member 在内存中的占用,经过哈希计算后最大占用 16 个字节。其实 murmur 相比 fnv hash 来说,性能是差点意思。
set 集合删除数据的实现
SRem
用来删除 set 集合里的 member 成员.
sremInternal
用来删除 set 集合里的 member 成员对象。其内部实现是计算 member 的 hash 值,然后从 radixTree 索引中剔除 key 为 hash 值的数据,然后在 logfile 里写一篇有 delete 标记的 entry 写入到 logfile 文件里,等待 rosedb 在 compact gc 时把数据给清理掉。
zset (sorted set 有序集合) 结构的设计实现
zset 是 rosedb 里最为复杂的数据结构了,其实在 redis 里除了 stream 外,zset 也是最复杂的那个。zset 是有序集合,不仅要实现有序,而且要做到去重集合的特性,单纯有序且去重的话,找个 avl, rbtree 都可以做到,java 的 hashmap 在触发阈值下也会从链表优化成红黑树。zset 的有序不是针对 member 排序,而是对关联的 score 分值做排序。
redis 的 zset 是使用 hashmap 和 skiplist 两个结构实现的,而 rosedb 则对每个 zset key 使用了两组数据结构,一个是 radixTree,一个是 hashmap + skiplist 组成的 sortedSet 结构。 radixTree 的 key 为 member 的哈希值,value 为 valuePos 值。 而 sortedSet 内的跳表使用 score 字段来排序,其每个 skiplist node 的 key 为 score 分值,而 value 为 memebr 的哈希值,另外 sortedSet 里的 hashmap 的 key 为 member 哈希值,则 value 是含有 member、score 的 skiplist node 节点。
rosedb zset 结构的实现有些冗余,其实直接使用 sortedset 就可以了,在 skiplist node 里增加 valuePos 字段即可。按照 zset 的实际应用场景,member 通常为一些标记字段,如数据库的 id 或分布式 id 等,其实无需计算成 16 字节的哈希值,还使用哈希值关联 logfile。其实直接把 member 原始值保存在内存索引中也没什么问题。
zset 写数据的实现
zset 获取 member 的 socre 实现
ZScore
用来获取 member 的 socre 分值。rosedb 没通过 zset 的 radixTree 来查询 valuePos,继而在到 logfile 里查询到 socre 分值。 其实 zset 内部通过跳表和hashmap 实现了类 redis 的 zset,分值的查询是依赖这个 zset 实现的。
zset 数据结构的代码位置: rosedb/ds/zset/zset.go
zset 删除 member 的实现
总结
到此,关于 rosedb 里 string、list、hash、set 和 zset 的数据结构的实现分析完了。 相比那些基于 lsmtree 封装的 redis 结构来说,基于 bitcask 实现这类结构还是相对简单不少,在 logfile 里写入 entry 数据,怎么写都可以,只需要再次启动时可以还原构建出索引即可,反而重点是如何在内存里构建 redis 的哪些数据结构。
string 是个简单的 kv,实现最简单,也最基础。list 是个链表结构,需要存储一个 metadata 记录了当前头尾的序号,后面的增删改查都依赖这个序号。hash 是个字典结构,radixtree 索引中记录 field 和 valuePos 的关系,而 logfile 的 key 为 key 和 field 的组合。 set 是个集合结构,radixtree 索引中记录了 member 的哈希值,而 logfile 记录了完整的 kv。zset 是 rosedb 里最为复杂的结构,其内部使用 sortedset 和 radixtree 来实现。
rosedb 里 hash 字典结构的设计
rosedb 里 set 集合结构的设计
rosedb 里 zset 有序集合结构的设计