数据库4:日新月异——版本管理
在说版本管理实现前,有两个问题得先问问自己:
- 什么是版本?
- 为什么需要版本管理?
版本其实就是LevelDB数据库的元数据,之前提到过,在MemTable读取不到键时,需要去SSTable读取。SSTable的文件有哪些,每一个文件分别在哪一个Level上面,每个文件里面包含的键的最小值最大值是什么。需要知道这些信息,才可以快速的从SSTable里读取出相应的键的值。而版本就保存了这些信息,让我们通过版本读取SSTable。
那么为什么要版本管理呢? 在数据不断写入后,MemTable写满了,这时候就会转换为Level 0的一个SSTable,或者Level n的一个文件和Level n + 1的多个文件进行Compaction,会转换成Level n + 1的多个文件。这会使SSTable文件数量改变,文件内容改变,也就是版本信息改变了,所以需要管理版本。
那么有一个版本就够了吗?在文件数量和内容改变时,修改当前的版本就行了。但是并不是,假设使用了整个数据库的迭代器,LevelDB在迭代数据库时,是提供了一个一致性的视图,也就是只能看到迭代前的写入,而迭代后的写入是看不到,在实现的时候,迭代器会引用这个版本,以及里面的SSTable,直到迭代结束。如果在迭代过程中,有文件删除了,那么就无法引用到这个文件了,就会出错。所以需要多个版本,有一个版本是当前版本,当新生成版本后,旧的版本如果有被其它线程引用,也需要保留,直到不被引用后,才会被删除,所以一个时刻可能有多个版本存在。
先看看LevelDB的版本管理架构图,可以看到主要用到了4个类:
Version
标识了一个版本,主要信息是这个版本包含的SSTable信息;VersionSet
是一个版本集,里面保存了Version
的一个双链表,其中有一个Version
是当前版本,因为只有一个实例,还保存了其它的一些全局的元数据,Dummy Version是链表头;VersionEdit
保存了要对Version
做的修改,在一个Version
上应用一个VersionEdit
,可以生成一个新的Version
;Builder
是一个帮助类,帮助Version
上应用VersionEdit
,生成新版本。
版本管理的基本工作流程如下:
VersionSet
里保存着当前版本,以及被引用的历史版本;- 当有Compaction发生时,会将更改的内容写入到一个
VersionEdit
中; - 利用
Builder
将VersionEdit
应用到当前版本上面生成一个新的版本; - 将新版本链接到
VersionSet
的双链表上面; - 将新的版本设置为当前版本;
- 将旧的当前版本
Unref
,就是引用计数减1。
版本控制中使用了引用计数来管理历史版本:
- 假设一个没有被访问的数据库,这时候只有一个版本,也就是当前版本v1,它的引用计数是1;
- 假设有一个迭代器开始访问数据库,这时候它会对当前版本v1
Ref
,引用计数变成了2; - 其它线程不断写入,导致了Compaction的生成,这时候生成了一个新的版本v2成为了当前版本,引用计数为1,然后对v1
Unref
,这时候v1的引用计数为1; - 因为v1的引用计数为1,所以不会被删除,迭代器线程还可以继续访问v1;
- 迭代器结束后,对v1进行
Unref
,这时候v1的引用计数变为0,就会从链表上删除了,这时候又只剩下v2版本了; - 版本里面的SSTable也是采用引用计数来管理的,每个版本引用一个SSTable会
Ref
,删除版本时会Unref
,如果一个SSTable的计数为0,那么这个SSTable就可以被删除了。
接下来讨论版本控制,以及这几个类的实现。
Version
Version
标识了一个版本,读取数据库时都需要使用Version
里面的版本信息,主要保存的是SSTable的文件信息:
// db/version_edit.h
struct FileMetaData {
FileMetaData() : refs(0), allowed_seeks(1 << 30), file_size(0) {}
int refs;
int allowed_seeks; // Compaction时会介绍这个字段
uint64_t number;
uint64_t file_size; // 文件大小
InternalKey smallest; // 文件里最小的inter