KV引擎-LevelDB
LevelDB
作为单机kv、基于LSM树的存储引擎, 其特点是高效,代码简洁而优美。在项目中的持久化数据的需求上目前既定使用LevelDB
进行操作,下面了解下整体架构和部分细节实现。
整体架构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CgZZEwRq-1666144013586)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5c3f72e102bc4920b4bd5997205c5699~tplv-k3u1fbpfcp-watermark.image?)]
从写入流程来分析
WAL
, 就是write Ahead Log, 用于保证数据的 持久性MemTable
, 内存存储结构对数据进行暂时的存储,内部的实现通过SkipList
实现。当MemTable
达到一定的大小之后转换为Immutable Table
等待后台线程写入磁盘SSTable
, 通过Compaction
生成的SSTable,分为不同的Level
,因此该存储引擎被称为LevelDB
Manifest
, 记录SSTable 的变更
亮眼1:数据编码压缩
整形定长数字编码
LevelDB中定长整型数字的编码,分为32bit & 64bit,下面着重分析32bit。
将定长的数字按照小端的方式进行顺序的编码,我们进行32bit整型的编码
🌰:首先看作是4个8位的数字:4,2,5,6
- 按照小端的顺序的话,buf[0]存储的就是6对应的数据
- 由于是小端,我们每次取的是最后面的8位,下一步先右移8位再取
- 同上
//低版本的PUT。。。直接将内容写入缓冲区
//要求:dst需要拥有足够的空间
inline void EncodeFixed32(char* dst, u_int32_t value) {
//类型转换,底层进行解释的不同,解释为:按照8位的类型进行解释
uint8_t* const buffer = reinterpret_cast<uint8_t>(dst);
/*
每次都会将最后面的一个字节清除掉,小端,存储到buf中
高地址---------低地址
4 2 5 6
buf[0]: |
buf[1]: |
buf[2]: |
buf[3]: |
*/
buffer[0] = static_cast<uint8_t>(value);
buffer[1] = static_cast<uint8_t>(value >> 8);
buffer[2] = static_cast<uint8_t>(value >> 16);
buffer[3] = static_cast<uint8_t>(value >> 24);
}
inline void EncodeFixed64(char* dst, uint64_t value) {
uint8_t* const buffer = reinterpret_cast<uint8_t*>(dst);
//同上述函数,将内容解析到buf中
buffer[0] = static_cast<uint8_t>(value);
buffer[1] = static_cast<uint8_t>(value >> 8);
buffer[2] = static_cast<uint8_t>(value >> 16);
buffer[3] = static_cast<uint8_t>(value >> 24);
buffer[4] = static_cast<uint8_t>(value >> 32);
buffer[5] = static_cast<uint8_t>(value >> 40);
buffer[6] = static_cast<uint8_t>(value >> 48);
buffer[7] = static_cast<uint8_t>(value >> 56);
}
整形变长编码
当整型值较小时,LevelDB支持将其编码为变长整型,以减少其空间占用(对于值与类型最大值接近时,变长整型占用空间反而增加)。
对于变长整型编码,LevelDB需要知道该整型编码的终点在哪儿。因此,LevelDB将每个字节的最高位作为标识符,当字节最高位为1时表示编码未结束,当字节最高位为0时表示编码结束。因此,LevelDB的整型变长编码每8位用来表示整型值的7位。因此,当整型值接近其类型最大值时,变长编码需要额外一字节来容纳原整型值。
亮眼2:跳表实现MemTable
LevelDB中跳表
基本概述
levelDB
中存储在内存中的数据就是存储在Memtable
中的, 其中Memtable
底层的数据结构就是基于 跳表来实现的。其中
Memtable
会有很频繁的插入和查询的操作,删除操作被模拟为是插入一个带有(删除)标签的数据,需要支持遍历操作
多线程的支持
多线程进行操作的时候我们需要保证的一些:
Writes require external synchronization, most likely a mutex.
Reads require a guarantee that the SkipList will not be destroyed
- Write:修改跳表的时候对用户进行加锁🔒
- Read:访问跳表的时候保证跳表不会被销毁掉
通过原子性的操作,使得能够实现 读读并发,读写并发线程安全, 但是 写写并发需要使用者自行维护
leveldb约定
- node不会被删除除非整个跳表结构都被销毁掉
- node节点除了next指针之外都是不变的