跳转至

我的内容

我的内容

这次我们讲解的主题是:创建启动数据库

需要回答的问题:

  1. 创建数据库的流程是什么?创建数据库时创建了哪些文件,都有什么作用?
  2. DBImpl类内都有哪些成员?其作用是什么?
  3. DB::Open函数的实现。
  4. DBImpl::Recover函数的实现。
  5. DBImpl::NewDB函数的实现。
  6. VersionSet::Recover函数的实现。CURRENT的作用是什么?

这次我们的分工:我后三个,林神前三个

林神的参考:https://zhuanlan.zhihu.com/p/340804308

https://leveldb-handbook.readthedocs.io/zh/latest/version.html

https://www.cnblogs.com/mh1092/p/9951342.html

https://blog.csdn.net/weixin_36145588/article/details/78029415

https://github.com/balloonwj/CppGuide/blob/master/articles/leveldb%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/leveldb%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%9018.md

https://blog.csdn.net/sinat_38293503/article/details/135661973#VersionSetRecoverbool_save_manifest_432

4.DBImpl::Recover函数的实现

总体来说,DBImpl::Recover做了以下事情

  • 创建数据库目录;

  • 对这个数据库里面的LOCK文件加文件锁。LevelDB是单进程多线程的,需要保证每次只有一个进程能够打开数据库,方式就是使用了文件锁,如果有其它进程打开了数据库,那么加锁就会失败

  • 如果数据库不存在,说明是新打开的DB,那么调用DBImpl::NewDB创建新的数据库;

  • 如果是重启DB,调用VersionSet::Recover来读取MANIFEST中每次版本的更改,恢复当前版本信息;

  • 根据版本信息,搜索数据库目录,找到关闭时没有写入到SSTable的日志,按日志写入顺序逐个恢复日志数据。

  • DBImpl::RecoverLogFile会创建一个MemTable,开始读取日志信息,将日志的数据插入到MemTable并根据需要调用DBImpl::WriteLevel0Table将MemTable写入到SSTable中


第一步,创建目录,然后尝试获取它的文件锁,失败则返回

image-20241014101312912

第二步,根据CURRENT文件是否存在,以及option参数执行检查。 如果文件不存在且create_if_missing这个参数为真,说明需要新创建数据库,调用函数NewDB()创建即可。

image-20241014101620267

第三步,调用VersionSet的Recover()函数,就是从文件中恢复数据。如果成功则向下执行第四步。

image-20241014102152371

第四步是重点,尝试从所有比manifest文件中记录的log要新的log文件中恢复,这是因为前一个版本可能会添加新的log文件,却没有记录在manifest中。最后如果发现有大于原信息记录的log编号的log文件,则需要回放log,更新db数据。回放期间db可能会dump新的level 0文件,因此需要把db元信息的变动记录到edit中返回。

首先,它先找出所有满足比manifest文件记录的log编号更新的log文件。之前的MANIFEST恢复,会得到版本信息,里面包含了之前的log number,搜索文件系统里的log,如果这些日志的编号 >= 这个log number,那么这些日志都是关闭时丢失的数据需要恢复。

image-20241014102601753

接下来,因为这里的日志是按顺序存储在logs里面,所以找到log文件后,首先排序,保证按照生成顺序,依次回放log。并把DB元信息的变动(sstable文件的变动)追加到edit中返回。这里有一点需要注意,就是前一版可能在生成该log编号后没有记录在MANIFEST中,所以这里我们手动更新VersionSet中的文件编号计数器

image-20241014103026678

最后,我们要更新VersionSet的sequence:

image-20241014103157621


5.DBImpl::NewDB函数的实现

第一步,产生DB元信息,设置比较器名称,当前分配日志文件的编号,下一个待分配的文件编号。这里有一个需要注意的,就是下一个待分配的文件编号是2,因为1分配给了MANIFEST文件。

image-20241014103633727

第二步,产生MANIFEST文件,将db元信息写入它。

image-20241014103814100

第三步,如果成功,就把MANIFEST文件名写入到CURRENT文件中,让CURRENT文件指向这个MANIFEST文件。

image-20241014103920036

可以看到,DBImpl::NewDB出人意料的简单,一个新的数据库没有任何数据,所以不需要日志和SSTable,只需要有一个MANIFEST文件,包含一些元数据就行了。最后CURRENT要指向新创建的MANIFEST文件。

6.VersionSet::Recover函数的实现。CURRENT的作用是什么?

概括来说,VersionSet::Recover会通过读取 CURRENT 文件,找到当前正在使用的 MANIFEST 文件,然后读取 MANIFEST 文件,将数据库恢复到上次关闭前的状态,完成了MANIFEST文件的读取和版本的构造。恢复出版本信息后,安装这个版本,那么数据库的元数据就恢复到了关闭时候的状态,这个数据库才准备好了可以读写了。当然这里要说一下我们恢复的是数据库哪些状态?我们需要知道数据库里有哪些SSTable文件,每个文件处于哪个Level,当前日志的编号等信息。

第一步,它要读取CURRENT文件的内容,获取当前使用的MANIFEST文件。在这里需要注意current的文件的格式问题,它正常来说应该以换行符做结尾。然后current中存放的是MANIFEST的文件名,要据此转化为完整路径才能打开

image-20241014105918835

第二步,要读取MANIFEST文件,将里面的VersionEdit读取应用到一个builder里。读取 MANIFEST 文件中的每一条 Record,把 Record 解码成一个 VersionEdit,然后调用 builder.Apply(&edit) 将 edit 应用到 builder 中。

image-20241014110447061

读取之后当然要关闭文件

image-20241014110548108

但是以上都不是VersionSet::Recover的核心部分。它的第三步是基于 MANIFEST 里的 VersionEdit列表,构造一个新的 Version,这一步尤为重要。

image-20241014111210091

这里有一点需要注意的,ReuseManifest检查是否可以继续使用当前的 MANIFEST 文件,为什么要这样做呢?如果MANIFEST可以重用,那么不需要保存MANIFEST。这里主要是通过MANIFEST的大小来判断,如果大于2M,那么就不会重用,而是将当前状态写入到一个新的MANIFEST文件里,这样可以避免打开的时候读取太大的MANINFEST,使得打开时间太长。如果不能继续使用的话,需要把当前的 MANIFEST 进行保存。

image-20241014111652370

image-20241014111453768

image-20241014111542216

CURRENT文件作用的说法1

随着更新与Compaction的进行,LevelDB会不断生成新文件,有时还会删除老文件,所以需要一个文件来记录文件列表,这个列表就是清单文件(manifest文件)的作用,清单会不断变化,DB需要知道最新的清单文件,必须将清单准备好后原子切换,这就是CURRENT文件的作用,LevelDB的清单过程更新如下:

  1. 递增清单序号,生成一个新的清单文件。
  2. 将此清单文件的名称写入到一个临时文件中。
  3. 将临时文件rename为CURRENT。

CURRENT 文件中存储的是当前正在使用的 MANIFEST 文件。

当创建新的 MANIFEST 文件时,LevelDB 会先更新 CURRENT文件,使其指向新的 MANIFEST 文件,然后再将旧的 MANIFEST 文件删除。

倘若没有 CURRENT 文件,新 MANIFEST 文件创建后,还没来得及删除旧的 MANIFEST 文件,系统就崩溃了,那么 LevelDB 恢复时就不知道当前正在使用的 MANIFEST 文件是哪个,也就无法恢复到正确的状态。