LevelDB是Google开源的持久化KV单机数据库,具有很高的随机写,顺序读/写性能,但是随机读的性能一般,LevelDB适合应用在查询较少,而写很多的场景。LevelDB应用了LSM(Log Structured Merge)策略,LSM-Tree对索引变更进行延迟及批量处理,并通过一种类似于归并排序的方式高效地将更新迁移到磁盘,降低索引插入开销

特点

  • Key和Value都是任意长度的字节数组

  • entry(一条KV记录)默认是按照key的字典顺序存储的,当然也可以自定义排序函数

  • 提供的基本操作接口:Put,Delete,Get,Batch

  • 支持批量操作以原子操作进行

  • 可以创建数据全景的snapshot,并允许在快照中查找数据

  • 可以通过前向(后向)迭代器遍历数据(迭代器会隐含创建一个snapshot)

  • 自动使用Snappy压缩数据

  • 可移植性

限制

  • 非关系型数据库(NoSQL),不支持SQL语句,也不支持索引
  • 一次只允许一个进程访问一个特定的数据库
  • 没有内置的CS架构

LevelDB

LevelDB是能够处理十亿级别规模Key-Value型数据持久性存储的C++程序库

  • 持久化存储
  • 根据记录的Key有序存储
  • 操作接口简单
  • 支持数据快照(snapshot)功能
  • 支持数据压缩

总体来说,LevelDB的写操作要大大快于读操作,而顺序读写操作则大大快于随机读写操作

整体架构

LevelDB分析 - 图1

六大部分

  • 内存:MemTableImmutable MemTable
  • 磁盘文件:Current文件,Manifest文件,Log文件,SSTable文件

Log文件用于系统崩溃恢复而不丢失数据,当应用写入一条KV记录时,LevelDB会先往Log文件写入,成功后将记录插进Memtable中,这样就完成了写入操作,一次写入操作只涉及一次磁盘顺序写和一次内存写

当Memtable写入足够多的数据后,就要写入磁盘文件,LevelDB会生成新的Log文件和Memtable,原先的Memtable就成为Immutable Memtable ,这个Memtable的内容是不可更改的,只能读,不能写入或删除。新来的数据被写到新的Log文件和Memtable,LevelDB后台调度会将Immutable Memtable的数据写入磁盘,形成一个新的SSTable文件。SSTable就是由内存中的数据不断导出并进行Compaction操作后形成的,而且SSTable的所有文件是一种层级结构,第一层为 Level 0,第二层为 Level 1,依次类推

SSTable文件的key是有序的,其中Level 0的两个SSTable文件可能存在key重叠,对于其它Level的SSTable文件来说,则不会出现同一层级内 .sst文件的key重叠现象

Manifest文件记载了SSTable各个文件的管理信息,包括属于哪个Level,文件名称,最小key和最大key

LevelDB分析 - 图2

Current文件记载当前的manifest文件名。因为在LevelDB的运行过程中,随着Compaction的进行,SSTable文件会发生变化,会有新的文件产生,老的文件被废弃,Manifest也会跟着反映这种变化,此时往往会新生成 Manifest文件来记载这种变化,而Current则用来指出哪个Manifest文件才是我们关心的那个Manifest文件

Log文件

从物理布局来讲,一个Log文件就是由连续的32K大小Block构成的

记录表示

LevelDB分析 - 图3

记录头包含三个字段,ChechSum是对类型和数据字段的校验码,记录长度记载了数据的大小,类型字段则指出了每条记录的逻辑结构和Log文件物理分块结构之间的关系,主要有四种类型:FULL/FIRST/MIDDLE/LAST

逻辑记录和物理 Block 之间的关系,LevelDB 一次物理读取为一个 Block,然后根据类型情况拼接出逻辑记录,供后续流程处理

LevelDB分析 - 图4

SSTable文件

SSTable文件的物理布局和逻辑布局结构

LevelDB 不同层级有很多 SSTable 文件(以后缀 .sst 为特征),所有 .sst 文件内部布局都是一样的。上节介绍 log 文件是物理分块的,SSTable 也一样会将文件划分为固定大小的物理存储块,但是两者逻辑布局大不相同,根本原因是:log 文件中的记录是 key 无序的,即先后记录的 key 大小没有明确大小关系,而 .sst 文件内部则是根据记录的 key 由小到大排列的,从下面介绍的 SSTable 布局可以体会到 key 有序是为何如此设计 .sst 文件结构的关键

LevelDB分析 - 图5

LevelDB分析 - 图6

从大的方面,可以将 .sst 文件划分为数据存储区和数据管理区,数据存储区存放实际的 Key:Value 数据,数据管理区则提供一些索引指针等管理数据,目的是更快速便捷的查找相应的记录。两个区域都是在上述的分块基础上的,就是说文件的前面若干块实际存储 KV 数据,后面数据管理区存储管理数据。管理数据又分为四种不同类型:紫色的 Meta Block,红色的 MetaBlock 索引和蓝色的数据索引块以及一个文件尾部块。

LevelDB 1.2 版对于 Meta Block 尚无实际使用,只是保留了一个接口,估计会在后续版本中加入内容,下面我们看看数据索引区和文件尾部 Footer 的内部结构。

LevelDB分析 - 图7

数据索引区的每条记录是对某个 Data Block 建立的索引信息,每条索引信息包含三个内容,以图 4.3 所示的数据块 i 的索引 Index i 来说:红色部分的第一个字段记载大于等于数据块 i 中最大的 key 值的那个 key,第二个字段指出数据块 i 在 .sst 文件中的起始位置,第三个字段指出 Data Block i 的大小(有时候是有数据压缩的),后面两个字段好理解,是用于定位数据块在文件中的位置的,第一个字段需要详细解释一下,在索引里保存的这个 key 值未必一定是某条记录的 key,以图 4.3 的例子来说,假设数据块 i 的最小 key=“samecity”,最大 key=“the best”;数据块 i+1 的最小 key=“the fox”,最大 key=“zoo”,那么对于数据块 i 的索引 Index i 来说,其第一个字段记载大于等于数据块 i 的最大 key(“the best”) 同时要小于数据块 i+1 的最小 key(“the fox”),所以例子中 Index i 的第一个字段是:“the c”,这个是满足要求的;而 Index i+1 的第一个字段则是 “zoo”,即数据块 i+1 的最大 key。

文件末尾 Footer 块的内部结构见图 4.4,Metaindex_handle 指出了 metaindex block 的起始位置和大小;index_handle 指出了 index Block 的起始地址和大小;这两个字段可以理解为索引的索引,是为了正确读出索引值而设立的,后面跟着一个填充区和魔数。

LevelDB分析 - 图8

LevelDB分析 - 图9

从图中可以看出,其内部也分为两个部分,前面是一个个 KV 记录,其顺序是根据 key 值由小到大排列的,在 Block 尾部则是一些“重启点”(Restart Point),其实是一些指针,指出 Block 内容中的一些记录位置。

“重启点”是干什么的呢?我们一再强调,Block 内容里的 KV 记录是按照 key 大小有序的,这样的话,相邻的两条记录很可能 key 部分存在重叠,比如 key i=“the car”,key i+1=“the color”,那么两者存在重叠部分 “the c”,为了减少 key 的存储量,key i+1 可以只存储和上一条 key 不同的部分 “olor”,两者的共同部分从 key i 中可以获得。记录的 key 在 Block 内容部分就是这么存储的,主要目的是减少存储开销。“重启点”的意思是:在这条记录开始,不再采取只记载不同的 key 部分,而是重新记录所有的 key 值,假设 key i+1 是一个重启点,那么key 里面会完整存储 “the color”,而不是采用简略的 “olor”方式。Block 尾部就是指出哪些记录是这些重启点的。

在 Block 内容区,每个 KV 记录的内部结构是怎样的?图 4.6 给出了其详细结构,每个记录包含 5 个字段:key 共享长度,比如上面的 “olor” 记录, 其 key 和上一条记录共享的 key 部分长度是 “the c” 的长度,即 5;key 非共享长度,对于 “olor” 来说,是 4;value 长度指出 Key:Value 中 value 的长度,在后面的 value 内容字段中存储实际的 value 值;而 key 非共享内容则实际存储 “olor” 这个 key 字符串。

LevelDB分析 - 图10

MemTable详解

LevelDB 的 MemTable 提供了将 KV 数据写入,删除以及读取 KV 记录的操作接口,但是事实上 Memtable 并不存在真正的删除操作,删除某个 key 的 value 在 Memtable 内是作为插入一条记录实施的,但是会打上一个 key 的删除标记,真正的删除操作是 Lazy 的,会在以后的 Compaction 过程中去掉这个 KV。

需要注意的是,LevelDB 的 Memtable 中 KV 对是根据 key 大小有序存储的,在系统插入新的 KV 时,LevelDB 要把这个 KV 插到合适的位置上以保持这种 key 有序性。其实,LevelDB 的 Memtable 类只是一个接口类,真正的操作是通过背后的 SkipList 来做的,包括插入操作和读取操作等,所以 Memtable 的核心数据结构是一个 SkipList。

SkipList 不仅是维护有序数据的一个简单实现,而且相比较平衡树来说,在插入数据的时候可以避免频繁的树节点调整操作,所以写入效率是很高的,LevelDB 整体而言是个高写入系统,SkipList 在其中应该也起到了很重要的作用。Redis 为了加快插入操作,也使用了 SkipList 来作为内部实现数据结构。

写入与删除记录

动态操作,比如读写记录,Compaction,错误恢复等操作

对于一个插入操作 Put(Key,Value) 来说,完成插入操作包含两个具体步骤:首先是将这条 KV 记录以顺序写的方式追加到之前介绍过的 log 文件末尾,因为尽管这是一个磁盘读写操作,但是文件的顺序追加写入效率是很高的,所以并不会导致写入速度的降低;第二个步骤是:如果写入 log 文件成功,那么将这条 KV 记录插入内存中的 Memtable 中,前面介绍过,Memtable 只是一层封装,其内部其实是一个 key 有序的 SkipList 列表,插入一条新记录的过程也很简单,即先查找合适的插入位置,然后修改相应的链接指针将新记录插入即可。完成这一步,写入记录就算完成了,所以一个插入记录操作涉及一次磁盘文件追加写和内存 SkipList 插入操作,这是为何 LevelDB 写入速度如此高效的根本原因。

从上面的介绍过程中也可以看出:log文件内是 key 无序的,而 Memtable 中是 key 有序的。那么如果是删除一条 KV 记录呢?对于 LevelDB 来说,并不存在立即删除的操作,而是与插入操作相同的,区别是,插入操作插入的是 Key:Value 值,而删除操作插入的是“Key:删除标记”,并不真正去删除记录,而是后台 Compaction 的时候才去做真正的删除操作。

读取记录

LevelDB 是针对大规模 Key/Value 数据的单机存储库,从应用的角度来看,LevelDB 就是一个存储工具。而作为称职的存储工具,常见的调用接口无非是新增 KV,删除 KV,读取 KV,更新 key 对应的 value 值这么几种操作。LevelDB 的接口没有直接支持更新操作的接口,如果需要更新某个 key 的 value,你可以选择直接生猛地插入新的 KV,保持 key 相同,这样系统内的 key 对应的 value 就会被更新;或者你可以先删除旧的 KV, 之后再插入新的 KV,这样比较委婉地完成 KV 的更新操作。

LevelDB 首先会去查看内存中的 Memtable,如果 Memtable 中包含 key 及其对应的 value,则返回 value 值即可;如果在 Memtable 没有读到 key,则接下来到同样处于内存中的 Immutable Memtable 中去读取,类似地,如果读到就返回,若是没有读到,那么只能万般无奈下从磁盘中的大量 SSTable 文件中查找。因为 SSTable 数量较多,而且分成多个 Level,所以在 SSTable 中读数据是相当蜿蜒曲折的一段旅程。总的读取原则是这样的:首先从属于 Level 0 的文件中查找,如果找到则返回对应的 value 值,如果没有找到那么到 Level 1 中的文件中去找,如此循环往复,直到在某层 SSTable 文件中找到这个 key 对应的 value 为止(或者查到最高 Level,查找失败,说明整个系统中不存在这个 key)。

那么为什么是从 Memtable 到 Immutable Memtable,再从 Immutable Memtable 到文件,而文件中为何是从低 Level 到高 Level 这么一个查询路径呢?道理何在?之所以选择这么个查询路径,是因为从信息的更新时间来说,很明显 Memtable 存储的是最新鲜的 KV 对;Immutable Memtable 中存储的 KV 数据对的新鲜程度次之;而所有 SSTable 文件中的KV数据新鲜程度一定不如内存中的 Memtable 和 Immutable Memtable 的。对于 SSTable 文件来说,如果同时在 Level L 和 Level L+1 找到同一个 key,level L 的信息一定比 level L+1 的要新。也就是说,上面列出的查找路径就是按照数据新鲜程度排列出来的,越新鲜的越先查找。

为啥要优先查找新鲜的数据呢?这个道理不言而喻,举个例子。比如我们先往 LevelDB 里面插入一条数据 {key=”http://www.samecity.com“ value=”朗格科技”},过了几天,samecity 网站改名为:69 同城,此时我们插入数据{key=”http://www.samecity.com“ value=”69同城”},同样的 key,不同的 value;逻辑上理解好像 LevelDB 中只有一个存储记录,即第二个记录,但是在 LevelDB 中很可能存在两条记录,即上面的两个记录都在 LevelDB 中存储了,此时如果用户查询 key=”http://www.samecity.com“,我们当然希望找到最新的更新记录,也就是第二个记录返回,这就是为何要优先查找新鲜数据的原因。

前文有讲:对于 SSTable 文件来说,如果同时在 Level L 和 Level L+1 找到同一个 key,Level L 的信息一定比 Level L+1 的要新。这是一个结论,理论上需要一个证明过程,否则会招致如下的问题:为神马呢?从道理上讲呢,很明白:因为 Level L+1 的数据不是从石头缝里蹦出来的,也不是做梦梦到的,那它是从哪里来的?Level L+1 的数据是从 Level L 经过 Compaction 后得到的(如果您不知道什么是 Compaction,那么……..也许以后会知道的),也就是说,您看到的现在的 Level L+1 层的 SSTable 数据是从原来的 Level L 中来的,现在的 Level L 比原来的 Level L 数据要新鲜,所以可证,现在的 Level L 比现在的 Level L+1 的数据要新鲜。

SSTable 文件很多,如何快速地找到 key 对应的 value 值?在 LevelDB 中,Level 0 一直都爱搞特殊化,在 Level 0 和其它 Level 中查找某个 key 的过程是不一样的。因为 level 0 下的不同文件可能 key 的范围有重叠,某个要查询的 key 有可能多个文件都包含,这样的话 LevelDB 的策略是先找出 level 0 中哪些文件包含这个 key(manifest 文件中记载了 Level 和对应的文件及文件里 key 的范围信息,LevelDB 在内存中保留这种映射表), 之后按照文件的新鲜程度排序,新的文件排在前面,之后依次查找,读出 key 对应的 value。而如果是非 Level 0 的话,因为这个 Level 的文件之间 key 是不重叠的,所以只从一个文件就可以找到 key 对应的 value。

最后一个问题,如果给定一个要查询的 key 和某个 key range 包含这个 key 的 SSTable 文件,那么 LevelDB 是如何进行具体查找过程的呢?LevelDB一般会先在内存中的 Cache 中查找是否包含这个文件的缓存记录,如果包含,则从缓存中读取;如果不包含,则打开 SSTable 文件,同时将这个文件的索引部分加载到内存中并放入 Cache 中。 这样 Cache 里面就有了这个 SSTable 的缓存项,但是只有索引部分在内存中,之后 LevelDB 根据索引可以定位到哪个Data Block 会包含这条 key,从文件中读出这个 Block 的内容,再根据记录一一比较,如果找到则返回结果,如果没有找到,那么说明这个 Level 的 SSTable 文件并不包含这个 key,所以到下一级别的 SSTable 中去查找。

从之前介绍的 LevelDB 的写操作和这里介绍的读操作可以看出,相对写操作,读操作处理起来要复杂很多,所以写的速度必然要远远高于读数据的速度,也就是说,LevelDB 比较适合写操作多于读操作的应用场合。而如果应用是很多读操作类型的,那么顺序读取效率会比较高,因为这样大部分内容都会在缓存中找到,尽可能避免大量的随机读取操作。

Compaction操作

前文有述,对于 LevelDB 来说,写入记录操作很简单,删除记录仅仅写入一个删除标记就算完事,但是读取记录比较复杂,需要在内存以及各个层级文件中依照新鲜程度依次查找,代价很高。为了加快读取速度,LevelDB 采取了 compaction 的方式来对已有的记录进行整理压缩,通过这种方式,来删除掉一些不再有效的 KV 数据,减小数据规模,减少文件数量等。

LevelDB 的 compaction 机制和过程与 Bigtable 所讲述的是基本一致的,Bigtable 中讲到三种类型的 compaction: minor,major 和 full。所谓 minor Compaction,就是把 Memtable 中的数据导出到 SSTable 文件中;major compaction 就是合并不同层级的 SSTable 文件,而 full compaction 就是将所有 SSTable 进行合并。

LevelDB 包含其中两种,minor 和 major。

先来看看 minor Compaction 的过程。Minor compaction 的目的是当内存中的 Memtable 大小到了一定值时,将内容保存到磁盘文件中,图 8.1 是其机理示意图。

LevelDB分析 - 图11 Compaction.jpg>)

从 8.1 可以看出,当 Memtable 数量到了一定程度会转换为 Immutable Memtable,此时不能往其中写入记录,只能从中读取 KV 内容。之前介绍过,Immutable Memtable 其实是一个多层级队列 SkipList,其中的记录是根据 key 有序排列的。所以这个 minor compaction 实现起来也很简单,就是按照 Immutable Memtable 中记录由小到大遍历,并依次写入一个 Level 0 的新建 SSTable 文件中,写完后建立文件的 index 数据,这样就完成了一次 minor compaction。从图中也可以看出,对于被删除的记录,在 minor compaction 过程中并不真正删除这个记录,原因也很简单,这里只知道要删掉 key 记录,但是这个 KV 数据在哪里?那需要复杂的查找,所以在 minor compaction 的时候并不做删除,只是将这个 key 作为一个记录写入文件中,至于真正的删除操作,在以后更高层级的 compaction 中会去做。

当某个 Level 下的 SSTable 文件数目超过一定设置值后,LevelDB 会从这个 Level 的 SSTable 中选择一个文件(Level>0),将其和高一层级的 Level+1 的 SSTable 文件合并,这就是 major compaction。

我们知道在大于 0 的层级中,每个 SSTable 文件内的 key 都是由小到大有序存储的,而且不同文件之间的 key 范围(文件内最小 key 和最大 key 之间)不会有任何重叠。Level 0 的 SSTable 文件有些特殊,尽管每个文件也是根据 key 由小到大排列,但是因为 Level 0 的文件是通过 minor compaction 直接生成的,所以任意两个 Level 0 下的两个 sstable 文件可能在 key 范围上有重叠。所以在做 major compaction 的时候,对于大于 Level 0 的层级,选择其中一个文件就行,但是对于 Level 0 来说,指定某个文件后,本 Level 中很可能有其它 SSTable 文件的 key 范围和这个文件有重叠,这种情况下,要找出所有有重叠的文件和 Level 1 的文件进行合并,即 Level 0 在进行文件选择的时候,可能会有多个文件参与 major compaction

LevelDB 在选定某个 Level 进行 compaction 后,还要选择是具体哪个文件要进行 compaction,LevelDB 在这里有个小技巧, 就是说轮流来,比如这次是文件 A 进行 compaction,那么下次就是在 key range 上紧挨着文件 A 的文件 B 进行 compaction,这样每个文件都会有机会轮流和高层 Level 的文件进行合并。

如果选好了 Level L 的文件 A 和 Level L+1 的文件进行合并,那么问题又来了,应该选择 Level L+1 哪些文件进行合并?LevelDB 选择 L+1 层中和文件 A 在 key range上有重叠的所有文件来和文件 A 进行合并。

也就是说,选定了 Level L 的文件 A,之后在 Level L+1 中找到了所有需要合并的文件 B,C,D……等等。剩下的问题就是具体是如何进行 major 合并的?就是说给定了一系列文件,每个文件内部是 key 有序的,如何对这些文件进行合并,使得新生成的文件仍然 key 有序,同时抛掉哪些不再有价值的 KV 数据

LevelDB分析 - 图12 compaction.jpg>)

Major compaction 的过程如下:对多个文件采用多路归并排序的方式,依次找出其中最小的 key 记录,也就是对多个文件中的所有记录重新进行排序。之后采取一定的标准判断这个 key 是否还需要保存,如果判断没有保存价值,那么直接抛掉,如果觉得还需要继续保存,那么就将其写入 Level L+1 层中新生成的一个 SSTable 文件中。就这样对 KV 数据一一处理,形成了一系列新的 L+1 层数据文件,之前的 L 层文件和 L+1 层参与 compaction 的文件数据此时已经没有意义了,所以全部删除。这样就完成了 L 层和 L+1 层文件记录的合并过程。

那么在 major compaction 过程中,判断一个 KV 记录是否抛弃的标准是什么呢?其中一个标准是:对于某个 key 来说,如果在小于 L 层中存在这个 key,那么这个 KV 在 major compaction 过程中可以抛掉。因为我们前面分析过,对于层级低于 L 的文件中如果存在同一 key 的记录,那么说明对于 key 来说,有更新鲜的 value 存在,那么过去的 value 就等于没有意义了,所以可以删除。

LevelDB中的Cache

书接前文,前面讲过对于 LevelDb 来说,读取操作如果没有在内存的 Memtable 中找到记录,要多次进行磁盘访问操作。假设最优情况,即第一次就在 Level 0 中最新的文件中找到了这个 key,那么也需要读取 2 次磁盘,一次是将 SSTable 的文件中的 index 部分读入内存,这样根据这个 index 可以确定 key 是在哪个 block 中存储;第二次是读入这个 block 的内容,然后在内存中查找 key 对应的 value。

LevelDB 中引入了两个不同的 Cache: Table Cache 和 Block Cache。其中 Block Cache 是配置可选的,即在配置文件中指定是否打开这个功能。

LevelDB分析 - 图13 Cache.jpg>)

图 9.1 是 Table Cache 的结构。在 Cache 中,key 值是 SSTable 的文件名称,value 部分包含两部分,一个是指向磁盘打开的 SSTable 文件的文件指针,这是为了方便读取内容;另外一个是指向内存中这个 SSTable 文件对应的 table 结构指针,table 结构在内存中,保存了 SSTable 的 index 内容以及用来指示 block cache 用的 cache_id,当然除此外还有其它一些内容。

比如在 get(key) 读取操作中,如果 LevelDB 确定了 key 在某个 Level 下某个文件 A 的 key range 范围内,那么需要判断是不是文件 A 真的包含这个 KV。此时,LevelDB 会首先查找 Table Cache,看这个文件是否在缓存里,如果找到了,那么根据 index 部分就可以查找是哪个 block 包含这个 key。如果没有在缓存中找到文件,那么打开 SSTable 文件,将其 index 部分读入内存,然后插入 Cache 里面,去 index 里面定位哪个 block 包含这个 key 。如果确定了文件哪个 block 包含这个 key,那么需要读入 block 内容,这是第二次读取。

LevelDB分析 - 图14 Cache.jpg>)

Block Cache 是为了加快这个过程的,图 9.2 是其结构示意图。其中的 key 是文件的 cache_id 加上这个 block 在文件中的起始位置 block_offset。而 value 则是这个 Block 的内容。

如果 LevelDB 发现这个 block 在 block cache 中,那么可以避免读取数据,直接在 cache 里的 block 内容里面查找 key 的 value 就行,如果没找到呢?那么读入 block 内容并把它插入 block cache 中。LevelDB 就是这样通过两个 cache 来加快读取速度的。从这里可以看出,如果读取的数据局部性比较好,也就是说要读的数据大部分在 cache 里面都能读到,那么读取效率应该还是很高的,*_而如果是对 key 进行顺序读取效率也应该不错,因为一次读入后可以多次被复用。_但是如果是随机读取,您可以推断下其效率如何。

Version、VersionEdit、VersionSet

Version 保存了当前磁盘以及内存中所有的文件信息,一般只有一个 Version 叫做 “current” version(当前版本)。LevelDB 还保存了一系列的历史版本,这些历史版本有什么作用呢?

当一个 Iterator 创建后,Iterator 就引用到了 current version(当前版本),只要这个 Iterator 不被 delete 那么被 Iterator 引用的版本就会一直存活。这就意味着当你用完一个 Iterator 后,需要及时删除它。

当一次 Compaction 结束后(会生成新的文件,合并前的文件需要删除),LevelDB 会创建一个新的版本作为当前版本,原先的当前版本就会变为历史版本。

VersionSet 是所有 Version 的集合,管理着所有存活的 Version。

VersionEdit 表示 Version 之间的变化,相当于 delta 增量,表示有增加了多少文件,删除了文件。下图表示他们之间的关系。

Version0 + VersionEdit —> Version1

VersionEdit 会保存到 MANIFEST 文件中,当做数据恢复时就会从 MANIFEST 文件中读出来重建数据。

LevelDB 的这种版本的控制,让我想到了双 buffer 切换,双 buffer 切换来自于图形学中,用于解决屏幕绘制时的闪屏问题,在服务器编程中也有用处

比如我们的服务器上有一个字典库,每天我们需要更新这个字典库,我们可以新开一个 buffer,将新的字典库加载到这个新 buffer 中,等到加载完毕,将字典的指针指向新的字典库

LevelDB 的 version 管理和双 buffer 切换类似,但是如果原 version 被某个 iterator 引用,那么这个 version 会一直保持,直到没有被任何一个 iterator 引用,此时就可以删除这个version