LevelDB 源码阅读笔记·初探 [0x01]

构建

首先 clone 代码:

1
git clone --recurse-submodules https://github.com/google/leveldb.git

由于 leveldb 使用了 googletest 和 benchmark,所以 clone 的时候要加上 --recurse-submodules

接下来我直接用 vscode 打开了 leveldb 目录,然后选定工具链, CMake 插件自动地完成了配置。

不得不说现在的 vscode 用起来非常顺手,构建、测试一步到位。

代码阅读方面我使用了 Sourcetrail,这是个很好的工具,可惜停更了,目前 Github 仓库已经归档了。

在读代码的时候还是需要对照着文档(leveldb/doc/index.md),这样便于理解函数的作用。

开始

首先来看一下核心的 leveldb/include/leveldb/db.h

开头 inlucde 了一些头文件

1
2
3
#include "leveldb/export.h"
#include "leveldb/iterator.h"
#include "leveldb/options.h"

这里 export.h 包含一些宏定义,这些都是常规操作,但作为初学者,我还是将这些记录下来

1
2
3
4
5
6
7
#if !defined(LEVELDB_EXPORT)

// 中间部分省略

#define LEVELDB_EXPORT
#endif

这样做防止了 LEVELDB_EXPORT 宏重复定义,确保函数和类成员只导出一次。

iterator.h 定义了迭代器,用于访问 Table 和 DB 对象。大概作用应该和 STL 里的迭代器有相似性。Iterator 中的 const 方法是线程安全的。

我们先不去管这个具体是怎么实现的,先放一放。

options.h 看名字就知道里面是一些选项,我们先不去管它。

回到 db.h,这里定义了几个类和结构体。

Snapshot: 大概是用于保存快照的,不过现在也不知道如何去用。

Range: 看不懂,跳过。

DB:这个类是 leveldb 库的核心,它定义了 DB 对象及一系列接口。这些会在下一节进行介绍。

最后是两个独立的函数 DestroyDBRepairDB,分别用于销毁 DB 对象和抢救打不开的 DB。

这里 DestroyDB 没有设计成 DB 类的方法,我想,这是因为这个函数比较危险,在使用时需要小心,如果设计成 DB 类的方法,可能一不小心就删库了。而作为一个独立的函数,增加了调用时的复杂度和明显度,这也是对用户的提醒与保护。

注意因为兼容性的原因,就算是 DestroyDB 方法没有成功,也可能会返回 Status::OK(),所以需要额外检查下错误信息。

DB 类的方法

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

首先是 Open 方法,用于打开数据库对象。值得注意的是,这里用到了双重指针,会在 dbptr 指针指向的地方创建一个指针,指向实际的数据库对象。我暂时还不知道这种设计有什么作用,先放一放,等之后了解全貌,这么设计的理由大概就会浮出水面。

接下来是 DB 类的构造函数和析构函数

1
2
3
4
5
6
DB() = default;

DB(const DB&) = delete;
DB& operator=(const DB&) = delete;

virtual ~DB();

DB() = default 是设置了一个默认的构造函数,这样符合 C++11 的规范。

DB 类在使用的时候,应当是独占访问的,不应该随意复制,所以将拷贝构造函数、拷贝赋值构造函数和都删除了。

1
2
3
4
5
6
7
8
9
10
virtual Status Put(const WriteOptions& options,
const Slice& key, const Slice& value) = 0;

virtual Status Delete(const WriteOptions& options,
const Slice& key) = 0;

virtual Status Write(const WriteOptions& options, WriteBatch* updates) = 0;

virtual Status Get(const ReadOptions& options,
const Slice& key, std::string* value) const = 0;

然后是经典的增删改查,我准备留到具体实现的时候来介绍。

1
virtual Iterator* NewIterator(const ReadOptions& options) = 0;

这个方法用于生成迭代器以访问 DB 对象,用起来和 STL 类似之处。这里贴出文档里的示例以供理解。

1
2
3
4
5
6
leveldb::Iterator* it = db->NewIterator(leveldb::ReadOptions());
for (it->SeekToFirst(); it->Valid(); it->Next()) {
cout << it->key().ToString() << ": " << it->value().ToString() << endl;
}
assert(it->status().ok()); // Check for any errors found during the scan
delete it;

迭代器要及时 delete,按照注释里的说法,迭代器要在 db 对象 delete 之前进行 delete。像这段示例,在迭代器用完之后就 delete 了。

1
2
virtual const Snapshot* GetSnapshot() = 0;
virtual void ReleaseSnapshot(const Snapshot* snapshot) = 0;

两个跟 Snapshot 有关的函数,先放一放。

1
virtual Status GetProperty(const Slice& property, std::string* value) const = 0;

这个方法用于获取数据库的属性,比如版本号,文件大小等。

1
2
virtual void GetApproximateSizes(
const Range* range, int n, uint64_t* sizes) const = 0;

这个方法用于获取数据库的文件大小,可能不包含最近写入的内容。

1
virtual void CompactRange(const Slice* begin, const Slice* end) = 0;

最后一个方法,用于压缩数据库,也留到具体实现的时候介绍。

关于纯虚函数

在阅读代码的时候,可以发现,db.h 中大多数函数都是纯虚函数,DB 类实际上是一个接口,具体的实现都在 DBImpl 类当中。

这是用到了多态性,在代码中使用的时候,使用 DB 的接口就可以,只需要

1
2
DB* db;
Status s = DB::Open(options, name, &db);

就可以得到一个 DB 对象,而不用关心 DB 对象的具体实现。

这里 leveldb 用到了工厂模式,隐藏了创建 DB 对象的过程。

这种方式有不少好处,一个是使用起来简单,不需要关注具体实现;还可以便于后期扩展,甚至说你哪怕底层根本不是 leveldb,代码里反正认的是这个接口,替换掉也不会影响已有代码。

小结

本节中我们了解了 db.h 中的一些接口,对 leveldb 的功能有了一个大致的了解。

接下来,我们将开始看 leveldb 的具体实现,了解它的具体实现。难度会比较大,估计要花上很长时间。


LevelDB 源码阅读笔记·初探 [0x01]
http://xiao-h.com/2024/06/06/Leveldb-0x01-初探/
作者
小H
发布于
2024年6月6日
许可协议