Blackwidow代码解析

背景

我们希望通过阅读 Blackwidow 源码来学习这个存储引擎存储数据的方式

1. String结构的存储

pika/src/storage/src/redis_strings.cc

截屏2023-07-11 09.58.58.png

String 本质上就是 Key, Value, 我们知道 RocksDB 本身就是支持 KV 存储的, 我们为了实现 Redis 中的 ttl 功能,所以在 value 后面添加了4 Bytes 用于存储 timestamp, 作为最后 RocksDB 落盘的 KV 格式,我们以一个 Setex 命令为例来看 KV 在 Blackwidow 下这个命令时怎么执行的,这里 ScopeRecordLock 给 key 上锁,防止多线程并发操作 key,db_->Put 操作的时候对 value 做了 Encode 操作(本篇中所有出现 Encode 的函数都代表对数据进行组装),我们先看下 SetRelativeTimestamp 这个函数.

截屏2023-07-11 10.01.29.png

我们可以看到 timestamp 由 unix_time 和 ttl 相加,unix_time 转换为 32 位,ttl 也是 32 位,timestamp_ 同样也是 32 位.可以看到这个是对 ttl 进行设置的,以 4 个字节存ttl的值,接着我们继续看 Encode 这个函数

src/storage/src/base_value_format.h

截屏2023-07-05 22.51.25.png

我们可以看到这里 start_ 是一个 char* 指针指向 dst ,再来看下 AppendTimestampAndVersion() 这个函数,由于这个函数在基类 (InternalValue)是一个虚函数里面没有写实现方法,所以我们调用的是派生类 (StringsValue) 覆写的这个方法.

截屏2023-07-05 23.01.37.png

这里调用 memcpy 函数把 user_value_ 的数据写进指针 dst 里面,然后指针后移 usize 大小,然后调用EncodeFixed32,这里由于我们是 setex 命令,所以这里的 timestamp 是之前调用 SetRelativeTimestamp() 函数存进的 ttl 值.(这边 kLittlwEndian 涉及到大小端法)

截屏2023-07-05 23.11.19.png

这里可以看出 EncodeFixed32 就是把 ttl 也写到 dst 里面,所以函数最终的返回就是 value 的大小外加一个 4 字节的 ttl.(value 组装完成) 如果我们没有对该 String 对象设置超时时间,则 timestamp 存储的值就是默认值 0, 否则就是该对象过期时间的时间戳, 每次我们获取一个 String 对象的时候, 首先会解析 Value 部分的后四字节, 获取到 timestamp 做出判断之后再返回结果。

所以 String 类型下的 key/value 就是以下构图:

截屏2023-07-05 23.23.09.png

2. Hash结构的存储

pika/src/storage/src/redis_hashes.cc

截屏2023-07-11 00.48.53.png

blackwidow 中的 hash 表由两部分构成,元数据(meta_key, meta_value), 和普通数据(data_key, data_value), 元数据中存储的主要是 hash 表的一些信息, 比如说当前 hash 表的域的数量以及当前 hash 表的版本号和过期时间(用做秒删功能), 而普通数据主要就是指的同一个 hash 表中一一对应的 field 和 value,作为具体最后 RocksDB 落盘的 KV 格式,我们以一个 Hset 命令看下在 blackwidow 怎么实现的 Hash 结构,这里我们以第一次使用为例,我们会先 Get 在 DB 中是否有存在的 key.

截屏2023-07-11 01.04.23.png

可以看到我们会走到 s.IsNotFound 这层逻辑(因为第一次使用 hset 肯定 key 是肯定不在 db 中的),然后我们看下EncodeFixed32 这个函数传入一个 str 和 1,这里的 1 代表当前 hash-size 的大小,因为一次 hset 只能 set 一个field 和对应的 value,所以第一次执行时 size 肯定是 1,而 EncodeFixed32 就是把整形的数字 1 以字符串的形式加到 str 中。

截屏2023-07-11 00.54.59.png

这里我们看到 memcpy 就是把整形数以字符串形式放到 buf 中.

截屏2023-07-11 00.59.46.png

处理完 hash-size 的构成,接下来是 Version 的部分,这里的 UpdataVersion 是派生类覆写的函数,可以看到这里的 version 依然是由 4 个字节组成,利用 static_cast 将 int64_t 转换为 32 位.

截屏2023-07-11 01.06.54.png

这个 Encode 完成对 meta-value 的组装,这里的部分和 KV 差不多,然后 AppendTimestampAndVersion 函数是调用派生类覆写的函数.这里我们看 AppendTimestampAndVersion 这个函数。

截屏2023-07-11 01.30.42.png

可以看到这里函数里面一次性完成了对 meta-value 的组装,第一个 memcpy 组装了 hash 的 size,后面两个EncodeFixed32 完成对 version 和 ttl 的组装,**(mate-value 组装完成)**

截屏2023-07-11 01.38.04.png

完成了对 meta-value 的组装,我们继续看下对 data-key 的组装,这里依旧是调用 Encode 函数

截屏2023-07-11 01.39.34.png

可以看到这里第一个 EncodeFixed32 装载 key 的 size,第二个 memepy 装载 key 的 data,第三个EncodeFixed32 装载 version,第四个 memcpy 装载 field 的 data. (data-key 组装完成)

如果我们需要查找一个 hash 表中的某一个 field 对应的 value, 我们首先会获取到 meta_value 解析出其中的timestamp 判断这个 hash 表是否过期, 如果没有过期, 我们可以拿到其中的 version, 然后我们使用 key, version,和 field 拼出 data_key, 进而找到对应的 data_value(如果存在的话)

所以 Hash 类型下的 key/value 就是以下构图:

Meta-key / Meta-value:

截屏2023-07-11 01.49.36.png

Data-key / Data-value:

截屏2023-07-11 10.17.07.png

3. List结构的存储

pika/src/storage/src/redis_list.cc

截屏2023-07-11 02.02.35.png

blackwidow 中的 list 由两部分构成,元数据(meta_key, meta_value), 和普通数据(data_key, data_value), 元数据中存储的主要是 list 链表的一些信息, 比如说当前 list 链表结点的的数量以及当前 list 链表的版本号和过期时间(用做秒删功能), 还有当前 list 链表的左右边界,普通数据实际上就是指的 list 中每一个结点中的数据,作为具体最后 RocksDB 落盘的 KV 格式,这里我们以 lpush 命令来看看 blackwidow 怎么存储 list 结构,这里我们以第一次使用为例,我们会先 Get 在 DB 中是否有存在的 key.

截屏2023-07-11 02.04.57.png

如果你仔细看了上文中的 blackwidow 的 hash 实现的话这里很容易看懂,首先是对 values 的组装,这里的 values是一个 vector,它不同于上面的 hash,lpush 命令可以一次 set 多个 value,所以将所有的 value 装在 vector中,所以这里就是把整形转为 char* 到 str 中,这里的 UpdateVersion 和上面的 hash 一样对 version 的设置

截屏2023-07-11 02.22.25.png

这里我们看到是先对 data-key 进行组装,第一个 EncodeFixed32 组装 key 的 size,第二个 memcpy 组装 key 的data,第三个 EncodeFixed32 组装 version,第四个 EncodeFixed64 组装 index. (data-key 组装完成)

截屏2023-07-11 02.26.20.png

接下来我们来看 meta-value 的组装,AppendTimestampAndVersion() + AppendIndex() 由这两个函数组成,我们先看 AppendTimestampAndVersion()

截屏2023-07-11 02.28.16.png

第一个 memcpy 组装 list 的size,第二个 EncodeFixed32 组装 version,第三个 EncodeFixed32 组装 ttl.三个都是4 字节

截屏2023-07-11 02.30.24.png

第一个 EncodeFixed64 组装 left_index,第二个 EncodeFixed64 组装 right_index.两个都是 8 字节

所以 List 类型下的 key/value 就是以下构图:

Meta-key / Meta-value:

截屏2023-07-11 02.39.55.png

Data-key / Data-value:

截屏2023-07-11 02.43.19.png

4. Set结构的存储

pika/src/storage/src/redis_sets.cc

截屏2023-07-11 02.47.08.png

blackwidow 中的 set 由两部分构成,元数据(meta_key, meta_value), 和普通数据(data_key, data_value), 元数据中存储的主要是 set 集合的一些信息, 比如说当前 set 集合 member 的数量以及当前 set 集合的版本号和过期时间(用做秒删功能), 普通数据实际上就是指的 set 集合中的 member,作为具体最后 RocksDB 落盘的 KV 格式,这里我们以 Sadd 命令看下 blackwidow 怎么实现 set 这种数据类型,这里我们以第一次使用为例,我们会先 Get 在DB 中是否有存在的 key.

截屏2023-07-11 02.49.19.png

由于是第一次 sadd,我们会走到 s.IsNotFound 这层逻辑里面,我们重点关注红色框里面的函数,这里EncodeFixed32 把整形 set 的大小 size 替换成 char*,

截屏2023-07-11 02.55.22.png

接着调用 Encode 函数对 meta-value 进行组装,这里的 AppendTimestampAndVersion() 我们看一下它的实现方法

截屏2023-07-11 02.56.28.png

第一个 memcpy 对 set 的大小组装,第二个 EncodeFixed32 对 version 组装,第三个 EncodeFixed32 对 ttl组装 (meta-value 组装完成)

截屏2023-07-11 02.58.55.png

对 data-key 进行组装,第一个 EncodeFixed32 对 key-size 组装,第二个 memcpy 对 key.data 组装,第三个EncodeFix32 对 version 组装,第四个 memcpy 对 member 进行组装. (data-key 组装完成)

所以 Set 类型下的 key/value 就是以下构图:

Meta-key / Meta-value:

截屏2023-07-11 03.08.25.png

Data-key / Data-value:

截屏2023-07-11 03.10.35.png

5. ZSet结构的存储

pika/src/storage/src/redis_zsets.cc

截屏2023-07-11 03.12.42.png

blackwidow 中的 zset 由两部部分构成,元数据(meta_key, meta_value), 和普通数据(data_key, data_value), 元数据中存储的主要是 zset 集合的一些信息, 比如说当前 zset 集合 member 的数量以及当前 zset 集合的版本号和过期时间(用做秒删功能), 而普通数据就是指的 zset 中每个 member 以及对应的 score, 由于 zset 这种数据结构比较特殊,需要按照 memer 进行排序,也需要按照 score 进行排序, 所以我们对于每一个 zset 我们会按照不同的格式存储两份普通数据, 在这里我们称为 member to score 和 score to member,作为具体最后 Rocksdb 落盘的KV 格式,我们以一个 Zadd 命令来看一下 blackwidow 怎么实现 zset 数据结构,这里我们以第一次使用为例,我们会先 Get 在 DB 中是否有存在的 key.

截屏2023-07-11 03.16.18.png

由于第一次执行 Zadd,我们会走到 s.IsNotFound 逻辑,这里 EncodeFixed32 是将 zset 的整形 size 大小转成char* 写进 buf,我们先看对 meta-value 进行组装的 Encode 函数。

截屏2023-07-11 03.20.01.png

这里我们先看下 AppendTimestampAndVersion 这个函数

截屏2023-07-11 03.21.12.png

这里第一个 memcpy 对 zset 的 size 进行组装,第二个 EncodeFixed32 对 version 进行组装,第三个EncodeFixed32 对 ttl 进行组装 (meta-value 组装完毕)

截屏2023-07-11 03.26.22.png

接下来我们来看按 member 落盘的 data-key,第一个 EncodeFixed32 对 key 的 size 组装,第二个 memcpy 对 key 的 data组装,第三个 EncodeFixed32 对 version进行组装,第四个 memcpy 对 member 进行组装 (data-key 按 membe 落盘组装完毕)

截屏2023-07-11 03.30.50.png

接下来我们来看按 score 落盘的 data-key,第一个 EncodeFixed32 对 key 的 size 组装,第二个 memcpy 对 key 的 data 组装,第三个 EncodeFixed32 对 version 进行组装,第四个 EncodeFix64 对 score 进行组装,第五个memcpy 对 member 进行组装 (data-key 按 score 落盘组装完毕)

所以 Zset 类型下的 key/value 就是以下构图:

Meta-key / Meta-value:

截屏2023-07-11 03.36.00.png

Data-key / Data-value By member:

截屏2023-07-11 03.37.35.png

Data-key / Data-value By score:

截屏2023-07-11 03.38.41.png