leveldb的整体思路

leveldb

Posted by bbkgl on November 24, 2019

三十功名尘与土

八千里路云和月

关于leveldb的整体思路,已经有超级多的博客讲过了,这里我只是浏览了很多资料,然后综合一下,做一个易理解的阐述。

数据接口

leveldb的接口十分简单,对于用户来说其实就是只有几个。

static Status Open(const Options& options,
                     const std::string& name,
                     DB** dbptr);

DB() { }
virtual ~DB();

// Set the database entry for "key" to "value".  Returns OK on success,
// and a non-OK status on error.
// Note: consider setting options.sync = true.
virtual Status Put(const WriteOptions& options,
                   const Slice& key,
                   const Slice& value) = 0;

// Remove the database entry (if any) for "key".  Returns OK on
// success, and a non-OK status on error.  It is not an error if "key"
// did not exist in the database.
// Note: consider setting options.sync = true.
virtual Status Delete(const WriteOptions& options, const Slice& key) = 0;

// Apply the specified updates to the database.
// Returns OK on success, non-OK on failure.
// Note: consider setting options.sync = true.
virtual Status Write(const WriteOptions& options, WriteBatch* updates) = 0;

// May return some other Status on an error.
virtual Status Get(const ReadOptions& options,
                     const Slice& key, std::string* value) = 0;

这几个接口的作用是显而易见的,分别对应了

  • Put:写入单个键值对,其实最后调用的还是Write
  • Delete:根据给的键值进行删除
  • Write:写入多个键值对,相当于多次Put
  • Get:根据给的键,读取对应的值

也就是我们常说的“增删改查”。

这里要说明的是,Put和Write的最大区别在于,Write写入多项数据,只有一项未成功,就不会写入,就是要么都成功,要么都失败,而Put仅是单项写入。可以看到,其实Write是包含了Put的功能的,所以Put实际上最后也是调的Write。

leveldb设计解决什么问题?

从上述四个“增删改查”的接口来看,其实不难,其实几百行代码就能解决,为什么leveldb会写出上万行代码呢?

如果要完成一个单机版的kv存储引擎,考虑需要解决以下几个问题:

  • 不能把所有数据都存入内存,需要将内存中数据同步到磁盘上
    • 内存存不下那么多数据
    • 断电后,内存中数据就丢失了,不能持久化存储
  • 如果将数据存储到磁盘上,怎么解决磁盘读取速度慢的问题?
  • 磁盘的随机读写速度要慢于顺序读写,怎么利用这点呢?

为了解决这三个问题,leveldb做了什么事情?

leveldb的设计

leveldb首先是把数据写入到内存中,当然是以WAL的方式写入,在硬盘上存储为SST文件,划分为不同的Level,Level0的文件由Memtable直接Dump产生,其他Level的SST文件则由上一层归并产生。

Memtable

Memtable对应leveldb暂存在内存中的数据,无论是读还是写,都会先访问内存中的数据。

Memtable的实现有如下特点:

  • 常驻内存
  • 使用跳表实现
  • 支持阻写状态下的遍历操作

跳表基于链表实现:

Log

数据写入Memtable之前,都会先写入Log,以免数据丢失。leveldb启动的时候会从log中回复Memtable的内容,所以Log模块得实现以下需求:

  • 磁盘存储
  • 大量的Append操作
  • 没有删除单条数据的操作
  • 遍历的读操作

Log文件划分为固定长度的Block,每个BLock包含多个Record;Record分为Record头和内容,Record头中记录了长度和校验码等信息。

SST文件

leveldb中把SST文件定义成Table,每个Table又划分为多个连续的Block,每个Block中又存储多条Entry:

磁盘上的block根据新旧先后分层。总是上面一层的与下面一层的合并。好吧,这里先不多说了,现在也不了解,先知道有这么个东西。

总结

总结上面说的,基本就是以下的思路:

  1. 写WAL LOG,也就是先写日志
  2. 日志写完后,更新内存,g也就是Memtable
  3. 当内存中存储的数据达到一定的量以后,就把Memtable变成不可变的内存块(方便和磁盘上的块合并)
  4. 把内存块上与磁盘上的Block,也就是.sst文件进行合并,到这里内存和磁盘上块的合并是顺序读写的
  5. 磁盘上的Block根据新旧先后分层,总是上一层与下一层合并
  6. 读的时候先查内存,内存中没有的时候,再顺次从Leve0~LevelN里面的磁盘查找,直到找到对应的Key,不存在则返回NULL