背景
我们希望通过阅读 Blackwidow 源码来学习这个存储引擎存储数据的方式
1. String结构的存储
pika/src/storage/src/redis_strings.cc
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 这个函数.
我们可以看到 timestamp 由 unix_time 和 ttl 相加,unix_time 转换为 32 位,ttl 也是 32 位,timestamp_ 同样也是 32 位.可以看到这个是对 ttl 进行设置的,以 4 个字节存ttl的值,接着我们继续看 Encode 这个函数
src/storage/src/base_value_format.h
我们可以看到这里 start_ 是一个 char* 指针指向 dst ,再来看下 AppendTimestampAndVersion() 这个函数,由于这个函数在基类 (InternalValue)是一个虚函数里面没有写实现方法,所以我们调用的是派生类 (StringsValue) 覆写的这个方法.
这里调用 memcpy 函数把 user_value_ 的数据写进指针 dst 里面,然后指针后移 usize 大小,然后调用EncodeFixed32,这里由于我们是 setex 命令,所以这里的 timestamp 是之前调用 SetRelativeTimestamp() 函数存进的 ttl 值.(这边 kLittlwEndian 涉及到大小端法)
这里可以看出 EncodeFixed32 就是把 ttl 也写到 dst 里面,所以函数最终的返回就是 value 的大小外加一个 4 字节的 ttl.(value 组装完成) 如果我们没有对该 String 对象设置超时时间,则 timestamp 存储的值就是默认值 0, 否则就是该对象过期时间的时间戳, 每次我们获取一个 String 对象的时候, 首先会解析 Value 部分的后四字节, 获取到 timestamp 做出判断之后再返回结果。
所以 String 类型下的 key/value 就是以下构图:
2. Hash结构的存储
pika/src/storage/src/redis_hashes.cc
blackwidow 中的 hash 表由两部分构成,元数据(meta_key, meta_value), 和普通数据(data_key, data_value), 元数据中存储的主要是 hash 表的一些信息, 比如说当前 hash 表的域的数量以及当前 hash 表的版本号和过期时间(用做秒删功能), 而普通数据主要就是指的同一个 hash 表中一一对应的 field 和 value,作为具体最后 RocksDB 落盘的 KV 格式,我们以一个 Hset 命令看下在 blackwidow 怎么实现的 Hash 结构,这里我们以第一次使用为例,我们会先 Get 在 DB 中是否有存在的 key.
可以看到我们会走到 s.IsNotFound 这层逻辑(因为第一次使用 hset 肯定 key 是肯定不在 db 中的),然后我们看下EncodeFixed32 这个函数传入一个 str 和 1,这里的 1 代表当前 hash-size 的大小,因为一次 hset 只能 set 一个field 和对应的 value,所以第一次执行时 size 肯定是 1,而 EncodeFixed32 就是把整形的数字 1 以字符串的形式加到 str 中。
这里我们看到 memcpy 就是把整形数以字符串形式放到 buf 中.
处理完 hash-size 的构成,接下来是 Version 的部分,这里的 UpdataVersion 是派生类覆写的函数,可以看到这里的 version 依然是由 4 个字节组成,利用 static_cast 将 int64_t 转换为 32 位.
这个 Encode 完成对 meta-value 的组装,这里的部分和 KV 差不多,然后 AppendTimestampAndVersion 函数是调用派生类覆写的函数.这里我们看 AppendTimestampAndVersion 这个函数。
可以看到这里函数里面一次性完成了对 meta-value 的组装,第一个 memcpy 组装了 hash 的 size,后面两个EncodeFixed32 完成对 version 和 ttl 的组装,**(mate-value 组装完成)**
完成了对 meta-value 的组装,我们继续看下对 data-key 的组装,这里依旧是调用 Encode 函数
可以看到这里第一个 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:
Data-key / Data-value:
3. List结构的存储
pika/src/storage/src/redis_list.cc
blackwidow 中的 list 由两部分构成,元数据(meta_key, meta_value), 和普通数据(data_key, data_value), 元数据中存储的主要是 list 链表的一些信息, 比如说当前 list 链表结点的的数量以及当前 list 链表的版本号和过期时间(用做秒删功能), 还有当前 list 链表的左右边界,普通数据实际上就是指的 list 中每一个结点中的数据,作为具体最后 RocksDB 落盘的 KV 格式,这里我们以 lpush 命令来看看 blackwidow 怎么存储 list 结构,这里我们以第一次使用为例,我们会先 Get 在 DB 中是否有存在的 key.
如果你仔细看了上文中的 blackwidow 的 hash 实现的话这里很容易看懂,首先是对 values 的组装,这里的 values是一个 vector,它不同于上面的 hash,lpush 命令可以一次 set 多个 value,所以将所有的 value 装在 vector中,所以这里就是把整形转为 char* 到 str 中,这里的 UpdateVersion 和上面的 hash 一样对 version 的设置
这里我们看到是先对 data-key 进行组装,第一个 EncodeFixed32 组装 key 的 size,第二个 memcpy 组装 key 的data,第三个 EncodeFixed32 组装 version,第四个 EncodeFixed64 组装 index. (data-key 组装完成)
接下来我们来看 meta-value 的组装,AppendTimestampAndVersion() + AppendIndex() 由这两个函数组成,我们先看 AppendTimestampAndVersion()
第一个 memcpy 组装 list 的size,第二个 EncodeFixed32 组装 version,第三个 EncodeFixed32 组装 ttl.三个都是4 字节
第一个 EncodeFixed64 组装 left_index,第二个 EncodeFixed64 组装 right_index.两个都是 8 字节
所以 List 类型下的 key/value 就是以下构图:
Meta-key / Meta-value:
Data-key / Data-value:
4. Set结构的存储
pika/src/storage/src/redis_sets.cc
blackwidow 中的 set 由两部分构成,元数据(meta_key, meta_value), 和普通数据(data_key, data_value), 元数据中存储的主要是 set 集合的一些信息, 比如说当前 set 集合 member 的数量以及当前 set 集合的版本号和过期时间(用做秒删功能), 普通数据实际上就是指的 set 集合中的 member,作为具体最后 RocksDB 落盘的 KV 格式,这里我们以 Sadd 命令看下 blackwidow 怎么实现 set 这种数据类型,这里我们以第一次使用为例,我们会先 Get 在DB 中是否有存在的 key.
由于是第一次 sadd,我们会走到 s.IsNotFound 这层逻辑里面,我们重点关注红色框里面的函数,这里EncodeFixed32 把整形 set 的大小 size 替换成 char*,
接着调用 Encode 函数对 meta-value 进行组装,这里的 AppendTimestampAndVersion() 我们看一下它的实现方法
第一个 memcpy 对 set 的大小组装,第二个 EncodeFixed32 对 version 组装,第三个 EncodeFixed32 对 ttl组装 (meta-value 组装完成)
对 data-key 进行组装,第一个 EncodeFixed32 对 key-size 组装,第二个 memcpy 对 key.data 组装,第三个EncodeFix32 对 version 组装,第四个 memcpy 对 member 进行组装. (data-key 组装完成)
所以 Set 类型下的 key/value 就是以下构图:
Meta-key / Meta-value:
Data-key / Data-value:
5. ZSet结构的存储
pika/src/storage/src/redis_zsets.cc
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.
由于第一次执行 Zadd,我们会走到 s.IsNotFound 逻辑,这里 EncodeFixed32 是将 zset 的整形 size 大小转成char* 写进 buf,我们先看对 meta-value 进行组装的 Encode 函数。
这里我们先看下 AppendTimestampAndVersion 这个函数
这里第一个 memcpy 对 zset 的 size 进行组装,第二个 EncodeFixed32 对 version 进行组装,第三个EncodeFixed32 对 ttl 进行组装 (meta-value 组装完毕)
接下来我们来看按 member 落盘的 data-key,第一个 EncodeFixed32 对 key 的 size 组装,第二个 memcpy 对 key 的 data组装,第三个 EncodeFixed32 对 version进行组装,第四个 memcpy 对 member 进行组装 (data-key 按 membe 落盘组装完毕)
接下来我们来看按 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:
Data-key / Data-value By member:
Data-key / Data-value By score: