最近我摸索出了给技术书籍做笔记的方法,于是也开始思考如何整理源代码阅读笔记。
2022-04-09:效果不太行,笔记有点简陋。参考这个博客,应该记录得更细致一点,不要介意贴大段代码。再附上一些值得学习的博客:youjiali、ying、rin、catkang
LevelDB不是SQL数据库,没有关系模型,不支持SQL语句,不支持索引。不能多个进程同时访问数据库,同一个进程的多个线程可以同时操作。
user_key 用户输入的key
InternalKey 数据库内部使用的Key,格式user_key SequenceNum Type
LookupKey 格式InternalKeyLength InternalKey
Varint32/64 对于数值小的数字,不需要4bytes、8bytes,会浪费空间。使用变长的表示方法,每7bit代表一个数,第8bit代表是否还有下一个字节。
Comparator 抽象类,要求派生类实现比较方法、用来找到比传入实参大的值的函数:FindShortestSeparator
和FindShortSuccessor
。
BytewiseComparatorImpl Comparator
的派生类,简单对字符串进行比较。
FindShortestSeparator(std::string* start, const Slice& limit
的一个参数如果是另一个的字串,则不做处理,否则返回公共字串拼接一个字符:(*start).substr(diff_index) ++ ((*start)[diff_index]+1)
FindShortSuccessor(std::string* key)
返回第一个字符+1:(*key)[i]+1
InternalKeyComparator Comparator
的派生类,对InternalKey
进行比较。持有用户定义的Comparator
。
比较user_key
部分会调用用户的Comparator
,默认使用BytewiseComparatorImpl
。如果user_key
相等,则依次对比SequenceNum
和Type
,这两者越大InternalKey
越小。
FindShortestSeparator
和FindShortSuccessor
也是调用用户的Comparator
,接着加上SequenceNum
和Type
。
Iterator 抽象类。允许注册清理函数RegisterCleanup
,可以注册多个,在迭代器析构时调用。
IteratorWrapper 持有Iterator* iter_
。提供和Iterator
同样的接口,将调用转发给iter_
。记录iter_
的key
和value
,避免多次调用虚函数。
TwoLevelIterator Iterator
的派生类,二级迭代器。持有指向第一层的IteratorWrapper index_iter_
,由构造函数初始化,和指向第二层的IteratorWrapper data_iter_
。迭代器操作都是在data_iter_
上。
构造时会传入一个返回Iterator*
的函数指针BlockFunction
,用来获得index_iter_
的value
指向对应容器的第二层迭代器data_iter_
。当index_iter_
更新,会调用BlockFunction
获得新的data_iter_
Arena 一个简单的内存池。其中类成员char* alloc_ptr_
指向当前空闲内存地址,size_t alloc_bytes_remaining_
记录当前块剩余容量,vector<char*> blocks_
,保存所有内存块的地址。
提供char* Allocate(size_t bytes)
的接口。当bytes
小于alloc_bytes_remaining_
,返回alloc_ptr_
。否则new
新的char
数组,新数组大小为4K或bytes
(当bytes
>1K),alloc_ptr_
指向新地址。注意,旧内存块剩余的空间会浪费掉。
也提供对齐Allocate
的接口,实现方式同上
SkipList 使用原子操作和模板技术实现跳表。不支持删除,不能插入重复值。使用Arena来分配新节点的内存。后趋节点数组使用柔性数组实现。上一层节点数是下一层的1/4。
未实现del操作是因为元素删除也是插入,删除某个Key的Value在 Memtable 内是作为插入一条记录实施的,但是会打上一个 Key 的删除标记,真正的删除操作是Lazy的,会在以后的 Compaction 过程中去掉这个KV。
MemTable 持有Arena
和SkipList
。引用计数,没有被引用时delete this
。跳表节点格式为LookupKey ValueLength Value
MemTableIterator Iterator
的派生类,对SkipList::Iterator
进行封装
WriteBatch 将所有的操作记录到rep_
,然后再通过WriteBatch::Iterate(Handler* handler)
一起提交。其中每次提交都会递增序列号。
WriteBatch::rep_ :=
sequence: fixed64
count: fixed32
data: record[count]
record :=
kTypeValue varstring varstring |
kTypeDeletion varstring
varstring :=
len: varint32
data: uint8[len]
其中WriteBatch
、WriteBatchInternal
、Handler
三者的关系可以学习一下
// include/leveldb/write_batch.h 暴露给用户的api
class WriteBatch {
public:
class Handler {
public:
virtual void Put(const Slice& key, const Slice& value) = 0;
...
};
Status Iterate(Handler* handler) const; // 将rep_中的数据通过handler提交出去
...
private:
friend class WriteBatchInternal;
};
// db/write_batch_internal.h 暴露给内部使用的api
class WriteBatchInternal {
public:
static Status InsertInto(const WriteBatch* batch, MemTable* memtable);
...
};
// db/write_batch.cc
namespace {
// 提交给MemTable的hander,同时会维护序列号,每次提交递增
class MemTableInserter : public WriteBatch::Handler { ... };
}
Status WriteBatchInternal::InsertInto(const WriteBatch* b, MemTable* memtable) {
MemTableInserter inserter;
...
return b->Iterate(&inserter);
}
每32K为一个Block
,Block
由Record
组成。格式如下:
Block := Record *
Record := Header Content
Header := Checksum Length Type
Type := kFullType | kFirstType | kMiddleType | kLastType
Type
用来表明当前Record
中的数据与要写入数据的关系。
Writer 提供AddRecord
接口,将数据按照上述格式写入文件,文件在构造时指定
Reader 提供ReadRecord
接口,按照上述格式读出文件的数据,文件在构造时指定
BloomFilterPolicy 布隆过滤器,key
是user_key
InternalFilterPolicy Wrapper
,处理InternalKey
,转而调用布隆过滤器
MergingIterator Iterator
的派生类,用于归并排序。类成员有数组children_
,数组对象是有序块的IteratorWrapper
。每次调用Next()
,类成员IteratorWrapper* current_
访问具有最小key
的块
BlockHandle 记录Block
的大小和偏移值。
Table 格式如图
Block 格式如图。顺序存储,因此可以二分查找。
利用有序数组相邻Key
可能有相同的前缀的特点来减少存储数据量。同时为了降低最开头数据损坏,其后的无法无法恢复的风险,还引入了重启点。
Data Block key
是InternalKey
Meta Block 格式如图
Data Block
每2KB生成一个filter
索引
Metaindex Block key
为filter.name
,Value
为该Meta Block
的BlockHandle
Index Block 记录Data Block
位置信息,key
为指向的Data Block
最后一条数据的Key
,Value
为该Data Block
的BlockHandle
Footer 解析的起点,大小固定
Cache 抽象类。
Cache::Handle Insert
等操作返回的句柄
LRUHandle HandleTable
和LRUCache
的节点,拥有前驱后趋指针。使用柔性数组存放key
。没继承Cache::Handle
?
HandleTable 使用拉链法的哈希表,节点是LRUHandle
LRUCache 持有HandleTable
来快速查找LRUHandle
节点,以及两个LRUHandle
链表 lru_
、in_use_
。被引用到的节点放在in_use_
中,当没被引用则转移到lru_
。
lru_
链表采用LRU算法进行管理。如果达到上限,则调用最久未使用节点的deleter
,并释放空间,这个deleter
在插入该节点时定义。
ShardedLRUCache Cache
的派生类,持有LRUCache
数组,对key
进行哈希分区到数组中,作用是减少多线程对同一个LRUCache
对象的争用。接口对LRUCache
进行简单封装。
TableCache 持有ShardedLRUCache
,缓存SSTable文件的句柄。ShardedLRUCache
的节点以文件编号为key
,{RandomAccessFile*, Table*}*
为value
,deleter
是清理value
中成员申请的内存。
当调用Get
函数找到key
,会使用found_key
、found_value
作为实参,调用传入Get
的函数指针实参handle_result
FileMetaData SSTable
文件的元信息,包括文件大小,文件编号,最大最小InternalKey
,引用计数(被不同Version
引用)
Version 记录当前版本所有文件的信息vector<FileMetaData*> files_[config::kNumLevels]
。作为VersionSet
的节点,拥有前驱和后趋的节点指针。析构会修改前驱和后趋指针、减少files_
中文件的引用,如果引用为零则delete
提供在当前版本所有文件进行操作的方法,如查找key
、MemTable
应该保存的层数等。
MemTable
不一定是保存在level0,能保存的层数为0、1、2。保存到高层的条件之一是,在底层没有重叠区间。
当前为level n,推向下一层level的条件是:与level n+1不能重叠,与level n+2重叠的文件大小不能超过阈值。
保存在第0层,可能导致文件过多,压缩和查找费时。保存到太高层(3+),会导致查找太深入,要打开的文件太多。如果该范围的更新比较频繁,往高层更压缩的开销也大。
Version::LevelFileNumIterator Iterator
的派生类,是Version::files_
中某一层的迭代器。key()
返回指向FileMetaData
的最大InternalKey
,value()
返回该FileMetaData
的文件编号和文件大小,
VersionEdit 保存相邻两个Version
的差异(比如vector<pair<int, FileMetaData>> new_files_
用来记录新增了哪些文件),并提供序列化反序列化VersionEdit
的接口。每次压缩完成,会向manifest
文件写入VersionEdit
manifest 中除了记录当前 memtable 对应的 log file 还需要记录 immutable memtable 的 log file,只有当 immutable memtable compaction 时 才可以删除对应的 log file。manifest 中记录的 sequence 并不是最新的,重启 db 时会根据 log file 恢复到最新。
manifest 在 db 打开时一直追加,不会进行清理,只有下一次打开时才会清理。若不幸 manifest 文件有所损坏或者被删除了,leveldb 也提供了修复的方式,所有的 metadata 除了 sstable 的组织结构外,都可以 通过 sstable 和 log 文件恢复,同时会将 log 转换为 sstable 并认为所有的 sstable 都处于 level-0,然后将修复后的 metadata 写入 manifest。会在打开 db 时立刻触发一次 compaction,因为 所有文件都在 level-0 所以 compaction 耗时会很久。
VersionSet 管理Version
链表,记录日志序号等状态信息。提供返回Compaction
的接口
重启时会根据manifest
中的VersionEdit
恢复并只保留最新的Version
,并清理无用数据。
VersionSet::Builder 用来应用VersionEdit
。调用Apply
会修改VersionSet::compact_pointers_
,记录VersionEdit
的deleted_files_
和new_files_
。当调用SaveTo
会在构造传入的Version
的files_
上加上没被删除的新文件
SnapshotList 维护双向链表,节点的值是SequenceNumber
。链表支持向末尾加入节点,以及删除任意节点。
在compaction
时会保留所有有可能被snapshot
访问到的数据。不会持久化snapshots
,重启时所有snapshots
就无效
Compaction vector<FileMetaData*> inputs_[2]
持有将要压缩的文件,提供一些压缩过程需要的信息
DBIter Iterator
的派生类,持有MergingIterator
,返回小于当前序列号的非删除user_key
。与MergingIterator
的差别在于,MergingIterator
的粒度是InternalKey
,会在user_key
相同的InternalKey
中遍历。DBIter
每次遍历的user_key
都不同。
遍历会随机抽样,检查是否执行压缩过程。如果该InternalKey
出现在不同层次,则减少位于最新层次SSTable
文件的计数器,当归零时触发压缩。
DBImpl 核心类,实现DB
的接口,主要做压缩相关的工作。
压缩可分为以下几种:
memtable
写入磁盘level
有重叠SSTable
文件进行压缩
挑选需要压缩文件的过程,会有一个轮转机制。记录上一次被压缩文件的位置,下一次从该位置开始查找,保证每一个文件都有被合并的机会。
smallest snapshot
能够访问到的及更高版本的,即保存第一个小于等于smallest snapshot
的版本及更高版本即可。- 若第一个小于等于
smallest snapshot
的版本是删除操作,只要高层没有这个key
也可以丢弃这个版本。