首页 > 数据库 > NoSQL > 如何看待 Todis 的数据存储编码格式?
简单的来说,对于原版 Pika , Blackwidow 的储存编码方案已经很优秀了;但可惜的是,对于 Todis 来说,原版储存编码方案存在一个致命的缺陷。我们不得不重新设计了现在的编码方式。
Todis 基本沿用了 原版 Blackwidow 的编码方案:对于除 string 之外的数据类型,均拆解为元数据 (meta_key , meta_value) 和 普通数据 (data_key , data_value) 。
区别在于:对于储存数据的 data_key ,原版编码方式采用了
KeySize + KeyData 来描述原始 key ,而 Todis 改为了双写原 key 的
0x00 ,并以 0x00 0x01 表示终止(以下称
"编码 Key" )。除了 data_key 里描述原始 key 的方式之外,其余部分与原版 Blackwidow 方案完全相同。
0. 编码 key
举一个简单栗子: 假设我们现在要存的 key 是 "0102507" 。(注意这里是字节意义上的,即每个 byte 的值)
在原版 Blackwidow 中,这个 key 会被编码为 " 00070102507" , 前 4 字节 " 0007" 是 KeySize , 后面的 7 个字节 "0102507" 是原始的 key 。(这里忽略了实际编码中后续的 version / index / member 等要素)
而在 Todis 中,我们会首先双写原 key 的每一个 0 , 变为 " 001 0025 007" ;再在末尾加上 终止符 "01" , 最终的编码为 " 001 0025 007 01" 。

容易看出,将排序后的原始 Key 均改写为编码 Key 后,所有的 Key 仍然是 有序 的。
另一个额外的好处是更节省空间,因为 0 字节 在 Redis 的 Key 中是极少出现的,因为绝大部分场景下,其 Key 都是 C 语言 string 格式(以 0 字节结尾,但不包括结尾的 0)。
1. String 结构的存储
String 本身就是一个 Key ,无需额外的 meta_data ,所有记录以普通数据的形式储存。
· String 结构 data_key 和 data_value 的落盘方式

data_value 包含原始 Value 和 4 Bytes 的有效时间戳 timestamp 。timestamp 主要用于支持 Redis 的 expire 功能,若未设置超时时间,则 timestamp 的值为 0 。
大部分情况下, timestamp 储存与对应结构的元数据中, String 比较简单因而记录在普通数据中。
· String 结构的查询方式
在查询时,会首先解析末尾的有效时间戳 timestamp 。若获取的 timestamp 已经过期,则不会返回数据。过期数据会在 compact 时被一并清理。
2. Hash 结构的存储
哈希表 的元数据储存了哈希表 Filed - Value 对的数量,版本号(创建时间戳)以及过期时间,普通数据则是该哈希表的每一对 Filed 和 Value 。
· Hash 结构 meta_key 和 meta_value 的落盘方式


data_key 由三个部分组成:编码 Key 、 4 Bytes 的版本号 Version 和该条记录中 Field 的内容。 data_value 就是该 Field 对应的 Value 。
· Hash 结构的查询方式
查找过程中,首先会获取到该 Key 的 meta_value ,解析出有效时间戳 timestamp 和版本号 version 。当数据没有过期时,将对应的编码 key 、 version 和 field 拼成 data_key ,再进行一次查询。
timestamp 超时或是 version 无效的数据(data_key 中的 version 与 meta_key 的 version 不一致)会在 compact 的过程中被清理。
3. List 结构的存储
Blackwidow 底层以数组的形式储存 List 的每一个节点。除了 Value 之外,还为每一个元素创建了索引。Blackwidow 保证同一列表的所有 节点 的索引是连续的。
列表的元数据储存了该列表的大小、版本号、过期时间以及索引范围,普通数据则储存该列表的每一个元素和它的索引。
· List 结构 meta_key 和 meta_value 的落盘方式

· List 结构 data_key 和 data_value 的落盘方式

data_key 由三个部分组成:编码 Key 、 4 Bytes 的版本号 Version 和 8 Bytes 的该节点的底层索引。 data_value 就是该列表元素对应的 Value 。
4. Set 结构的存储
Set 结构的元数据中储存了 集合 的大小、版本号和过期时间,每个集合的 member 储存为普通数据。
· Set 结构 meta_key 和 meta_value 的落盘方式


data_key 由三个部分组成:编码 Key 、 4 Bytes 的版本号 Version 和该条记录中 Member 的内容。集合只需要记录 member ,data_value 目前为 空串 。
5. ZSet 结构的存储
Zset 结构比较特殊,既需要支持按照 Score 排序,也需要支持按照 Member 排序,故 Zset 中的每一个数据都会在普通数据中被写两遍,我们分别将其称为 ZsetScoreKey 和 ZsetMemberKey 。这两种 Key 都储存了某一条 Score - Member 的全部信息,但 ZsetScoreKey 落盘后按 Score 排序,而 ZsetMemberKey 按 Member 排序。
Zset 的元数据中储存 Zset 的大小、版本号和过期时间。
· Zset 结构 meta_key 和 meta_value 的落盘方式

· Zset 结构中描述 ZsetMemberKey 的 data_key 和 data_value 的落盘方式

data_key 由三个部分组成:编码 Key 、 4 Bytes 的版本号 Version 和该条记录中 Member 的内容。 与Set 不同, 可以在 data_value 中储存该 Member 对应的 Value 。
由于 RocksDB 默认按照 字节序 排序,因此这样排布后,同一 Key 下的所有记录的开头部分均相同,必定是按照 Member 排序的。
· Zset 结构中描述 ZsetScoreKey 的 data_key 和 data_value 的落盘方式

data_key 由四个部分组成:编码 Key 、 4 Bytes 的版本号 Version 、该条记录的 Score 以及 Member 。由于 data_key 中储存了所有的信息,因此这里的 data_value 依然为空。
针对 ZsetScoreKey , Blackwidow 实现了它对应的 comparator ,同一个 Key 的 ZsetScoreKey 会先按照 Score 的大小排序,再按照 Member 排序。
为什么采取新的编码方案
· Todis 编码方案的优点
// 仅保留了主要逻辑, BaseData 可以用于构建 Hash 、 Set 、 ZsetMemberKey 的 data_keyBaseDataFilter::Filter(data_key, data_value) {
[meta_key, data_version] = parse_data_key(data_key);
[meta_version, meta_timestamp] = parse_meta_value(get_from_meta_cf(meta_key));
if (meta_timestamp && meta_timestamp < curr_time)
return true;
if (meta_version > data_version)
return true;
return false;
}
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70013015/viewspace-2908022/ ,如需转载,请注明出处,否则将追究法律责任。