这篇文章是levelDB官方文档的译文,原文地址:LevelDB library documentation
这篇文章主要讲leveldb接口使用和注意事项。
leveldb是一个持久型的key-value数据库。key,value可以是任意的字节数组,key之间是有序的。key的比较函数可以由用户指定。
1. 打开数据库
leveldb使用文件系统目录名作为name,并把数据库所有内容都存储在这个目录中。这是个打开数据库,并且指定如果数据库不存在就新建的例子:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
如果想在创建数据库时发现已经存在就报错,那么调用leveldb::DB::Open
之前添加下面这一行:
- 1
- 1
2. 返回值状态Status
你也许已经注意到上面的leveldb::Status type
. 这种类型是leveldb大多数函数的返回值,函数的返回值有可能是error。可以通过判断result是不是ok来判断:
- 1
- 2
- 1
- 2
3. 关闭数据库
当数据库操作完成,关闭数据库只需要删除数据库对象:
- 1
- 2
- 3
- 1
- 2
- 3
4. 读和写
数据库提供Put, Delete, and Get
函数来修改查询数据库。下面的例子把key1的值赋给key2:
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
5. 原子性更新
如果上面的代码中,再给key2赋值之后,删除key1之前,进程退出了,那么key1 and key2就会有相同的值。这种情况可以使用WriteBatch class
来原子性的进行一系列的更新:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
WriteBatch是一系列对数据库的更新操作,并且这些批量操作之间有一定的顺序性。注意到我们虽然在给key2赋值之前删除,使用writebtch最终并不会错误的造成vaue丢失。
撇开writebatch带来的原子性优势,writebatch也能通过把多个更新放在一个批量操里面来加速操作。
6. 同步写
通常情况下,所有的leveldb写操作都是异步的:当leveldb把写操作交个操作系统之后就返回。从操作系统内存到硬盘等持久性存储是异步的。如果在写的时候打开同步写选项,那么只有当数据持久化到硬盘之后才会返回。(On Posix systems, this is implemented by calling either fsync(...)
orfdatasync(...)
or msync(..., MS_SYNC)
before the write operation returns.)
- 1
- 2
- 3
- 1
- 2
- 3
异步写通常比同步写快1000倍以上。异步写的不足就是当机器宕机时会丢失最后更新的数据。写进程的异常退出并不会造成数据的丢失。
通常情况下异步写能够被妥善的处理。例如,当你在网数据库写大量的数据时,在机器宕机之后能通过重新写一次数据来修复。混合使用同步和异步也是可以的。例如每N次写做一次同步。当机器宕机的时候,只需要重新写最后一次同步写之后的数据。同步写一个新增一个标记来记录上一次同步写的位置。
WriteBatch是一个异步写。一个WriteBatch内部的多个更新操作放在一起也可以使用同步写操作,(i.e., write_options.sync
is set to true). 可以通过批量操作降低同步写的消耗。
7. 并发
一个数据库每次只能被一个进程打开。leveldb为了防止误操作需要一个lock。在一个进程内部,同一个leveldb::DB
对象可以在这个进程的多个并发线程之间安全的共享。 例如,不同的线程可以写,获取指针,或者读取相同的数据库,而不需要额外的同步操作,因为leveldb自动做了请求的同步。然而,其他的对象,例如迭代器或者WriteBatch,需要外部的同步操作。如果两个线程共享同一个这样的对象,那么他们必须用自己的lock protocal对数据库操作进行保护。这在公共的header文件里有更详细的内容。
8. 迭代器
下面的例子说明了如何输出数据库的所有key-value对:
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
处理[start,limit)范围内的key:
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
逆序处理:(逆序会比顺序慢一些)
- 1
- 2
- 3
- 1
- 2
- 3
9. Snapshots快照
快照在整个key-value存储状态上提供了一个持久性的只读视图。非空的ReadOptions::snapshot
提供了一个针对db特定状态的只读视图。如果ReadOptions::snapshot
是NULL,那么读操作是在对当前数据库状态的隐式视图上的进行的。
使用DB::GetSnapshot()
方法创建Snapshots:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
如果快照不再需要了,应该使用DB::ReleaseSnapshot
接口来释放,这会消除为了维持快照的状态多与操作。
10. Slice分片
上面代码中it->key()
and it->value()
调用的返回值都是leveldb::Slice
类型的实例,slice是一个包含长度和一个纸箱字节数组的简单结构体。因为我们不需要每次都复制很多的keys和values,所以返回Slice比返回std::string是一个更好的选择。另外,level-db不返回以null结尾的c类型的字符串,是因为leveldb允许key和value中包含'\0'
字符。
C++ strings和null-terminated C-style strings可以很容易的转换为Slice:
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
A Slice can be easily converted back to a C++ string:
- 1
- 2
- 1
- 2
使用slice的时候需要仔细,需要确保在使用slice的时候,他的指针所指向的地址是有效的。例如,下面不正当的使用:
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
在if的作用域外,str已经被销毁了,所以slice所指向的内存地址已经被释放了。
11. Comparators
前面的例子都是使用默认的排序函数,也就是字典序。另外,我们也在打开数据库的时候也可以指定一个排序比较函数。
例如,假设数据库的key由两个数字组成,我们首先用第一个数字排序,第一个数相等时使用第二个数比较。首先,我们先定义一个leveldb::Comparator
的子类来实现我们的想法:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
使用自定义的comparator创建数据库:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
12. 后向兼容
当数据库创建的时候comparator的Name()函数也附加在数据库上,并且后续每次打开的时候都会进行检查。如果comparator的name变了,leveldb::DB::Open
就会失败。因此,当且仅当1,新的key格式和比较函数和现存的数据库不兼容,2,丢弃当前的数据库的所有内容也无所谓。的时候才会修改comparator的name.
不过你仍然可以逐渐的进化key的格式。例如,你可以存储为每个key存储一个版本号,(多数情况下一个字节足够用),当想使用一个新的key格式的时候,a,使用相同的comparator name,b,为新的key格式增加版本号,c,修改comparator函数,能通过key里面的版本来判断怎么解析key。
13. 性能
可以通过修改include/leveldb/options.h
里面参数的默认值进行性能调优。
13.1 块大小
leveldb把相邻的key放进同一个block,block是读写数据库时的单元。默认的未压缩block大小是4KB。那些经常对数据库做块读取的应用希望增加块的大小。如果把块大小调小对性能有提升的话,那些经常随机数据库的应用会希望减小块的大小。一般来说,块小于1KB或者大于几MB是无益的。并且,当块比较大的时候数据压缩效率会高一些。
13.2 压缩
每个块在写到持久存储之前是独立进行压缩的。由于默认的压缩算法非常快,所以压缩默认是打开的。并且,对于不可压缩的数据,也会自动停止压缩。在很少的情况下,应用程序可能会完全禁用压缩,但是除非benchmark表明性能有提成否则不建议这么做。
- 1
- 2
- 3
- 1
- 2
- 3
13.3 Cache
数据库的内容存储在文件系统上的一系列文件中。每个文件里面有很多的压缩数据块。如果options.cache
是非空的,那么数据库会使用cache来缓存经常使用的未压缩的数据块。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
cache存的是未压缩的数据,因此,cache需要根据应用层的数据大小,计算应该缓存的数据量。当进行批量读的时候,应用可能会希望禁用cache,以防止批量读的数据不要把已经缓存的内容替换掉。一个顺序指针可以满足要求:
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
13.4 Key Layout
数据传输单位和缓存单位都是数据块。根据数据库排序算法,相邻的key一般情况下会放在同一个数据块里面。因此,通过把经常一起访问的相邻key放在一个block里面,把不经常使用的key放在分隔的块里面,应用程序可以提升性能。
例如,我们在leveldb之上实现一个文件系统。我们希望entry的类型会这样存储:
- 1
- 2
- 1
- 2
我们希望文件名的前缀是某个字符,如’/’,file_block_id的前缀是不同的字符,例如’0’,这样我们就能只浏览metadate,而不用强制缓存大量的文件内容了。
13.5 过滤器Filters
由于leveldb在磁盘上组织数据的方式,一个Get()
调用可能导致多次磁盘读操作。可选的FilterPolicy机制可以潜在的减少磁盘读操作。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
上面的代码在数据库中使用一个基于Bloom filter的filtering策略。基于Bloom filter的filtering策略为每个key在内存保存一些数据位,这上面的例子中,为每个key保存10位),这个filter可以降低Get()
调用操作中不必要的磁盘读大概100倍。增加每个key的位数可以更大的降低磁盘读,但是会增加内存的使用。建议那些工作集不适合放在内存的应用,以及随机读比较多的应用使用filter policy。
如果你使用的是自定义的comparator,应该确保filter policy和comparator是兼容的。例如,一个在key进行比较的时候会删除前后的空格的comparator。应用程序也应该提供一个忽略前后空格的自定义filter policy。例如:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
高级应用程序可以使用不依赖 bloom filter的策略,或者根据key集合的特征提供自己的filter policy。详情可以参照leveldb/filter_policy.h
。
14. 校验和
leveldb把校验和和存储在文件系统中的数据联系起来。下面是两种校验和验证的方式:
ReadOptions::verify_checksums
可以设置为true,来强制对从文件系统读取的所有数据进行校验和验证。默认不使用。
Options::paranoid_checks
可以在打开数据库之前设置为true,来确保一旦检测到内部错误就尽快抛出异常。当数据库打开的时候可能抛出异常,或者后续的数据库操时抛出。默认情况下,会禁用多疑的检测,这样的话,即使部分持久性存储崩溃数据库依旧可以使用。
如果数据库崩溃了,如果多疑检测打开的话,可能无法打开这个数据库,可以使用leveldb::RepairDB
函数来修复尽可能多的数据。
15. 空间估算
GetApproximateSizes
方法可以用来估算一个或多个key占用文件系统空间。
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
上面的代码中,size[0]是key范围在[a..c)之间的内容占用的文件空间的大小估算值。sizes[1]是key范围在[a..c)之间的内容占用的文件空间的大小估算值。
16. 环境变量
leveldb所有文件操作和其他的系统调用通过leveldb::Env
对象来判断如何使用,复杂的客户端可能希望提供自己的Env实现做到更好的控制。例如,应用程序可以人为为文件IO操作增加延时以降低leveldb对系统的其他应用带来的影响。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
17. 可移植性
通过提供leveldb/port/port.h
引用的types/methods/functions
的平台特定实现,leveldb就能移植到新的平台。leveldb/port/port_example.h
里面有更详细的内容。
另外,新平台可能需要新的leveldb::Env
实现。See leveldb/util/env_posix.h
for an example.
18. 其他
leveldb的更多实现细节可以看下面的文档:
levelDB实现细节
levelDB immutable Table的文件格式
leveldb日志文件格式