LevelDB 基础操作

基本操作

环境搭建

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 下载源码
git clone https://github.com.cnpmjs.org/google/leveldb.git

# 下载依赖第三方库(benchmark、googletest)
git submodule update --init 

# 执行编译
cd leveldb/
mkdir -p build && cd build 
cmake -DCMAKE_BUILD_TYPE=Debug .. && cmake --build .

# 头文件加入系统目录
cp -r ./include/leveldb /usr/include/
cp build/libleveldb.a /usr/local/lib/

实战使用

创建、关闭数据库

创建数据库或打开数据库,均通过 Open 函数实现。Open 为一个静态成员函数,其函数声明如下所示:

1
2
static Status Open(const Options& options, const std::string& name,
                 DB** dbptr);
  • const Options & options:用于指定数据库创建或打开后的基本行为。
  • const std::string & name:用于指定数据库的名称。
  • DB** dbptr:定义了一个DB类型的指针的指针,该指针作为Open函数操作后传给调用者使用的DB类型的实际指针。
  • Status:当操作成功时,函数返回 status.ok() 的值为 True,*dbptr 分配了不为 NULL 的实际指针地址;若其中的操作存在错误,则 status.ok() 的值为 False,并且对应的 *dbptr 为 NULL。

关闭数据库非常简单,只需要使用 delete 释放 db 即可。

代码示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include"leveldb/db.h"
#include<string>
#include<iostream>

using namespace std;

int main()
{
  leveldb::DB* db;
  leveldb::Options op;
  op.create_if_missing = true;

  //打开数据库
  leveldb::Status status = leveldb::DB::Open(op, "/tmp/testdb", &db);
  if(!status.ok())
  {
      cout << status.ToString() << endl;
      exit(1);
  }

  //关闭数据库
  delete db;

  return 0;
}

数据读、写、删除

数据库中的读、写与删除操作分别用 GetPutDelete 这3个接口函数实现。接口定义如下:

1
2
3
4
5
Status Get(const ReadOptions& options, const Slice& key,
         std::string* value);
Status Put(const WriteOptions&, const Slice& key,
         const Slice& value);
Status Delete(const WriteOptions&, const Slice& key);
  • ReadOptions& options:代表实际读操作传入的行为参数。
  • WriteOptions& options:代表实际写操作传入的行为参数。

这里需要注意的是 Delete 并不会直接删除数据,而是在对应位置插入一个 key 的删除标志,然后在后续的Compaction 过程中才最终去除这条 key-value 记录。

代码示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include"leveldb/db.h"
#include"leveldb/slice.h"
#include<iostream>

using namespace std;

int main()
{
  leveldb::DB* db;
  leveldb::Options op;
  op.create_if_missing = true;

  //打开数据库
  leveldb::Status status = leveldb::DB::Open(op, "/tmp/testdb", &db);
  if(!status.ok())
  {
      cout << status.ToString() << endl;
      exit(1);
  }

  //写入数据
  leveldb::Slice key("hello");
  string value("world");
  status = db->Put(leveldb::WriteOptions(), key, value);
  if(status.ok())
  {
      cout << "key: " << key.ToString() << " value: " << value << " 写入成功。" << endl;
  }

  //查找数据
  status = db->Get(leveldb::ReadOptions(), key, &value);
  if(status.ok())
  {
      cout << "key: " << key.ToString() << " value: " << value << " 查找成功。" << endl;
  }

  //删除数据
  status = db->Delete(leveldb::WriteOptions(), key);
  if(status.ok())
  {
      cout << "key: " << key.ToString() << " 删除成功。" << endl;
  }

  //关闭数据库
  delete db;

  return 0;
}

批量处理

针对大量的操作,LevelDB 不具有传统数据库所具备的事务操作机制,然而它提供了一种批量操作的方法。这种批量操作方法主要具有两个作用:一是提供了一种原子性的批量操作方法;二是提高了整体的数据操作速度。

LevelDB 针对批量操作定义了 WriteBatch 的类型。WriteBatch 有 3 个非常重要的接口:数据写(Put)、数据删 除(Delete)以及清空批量写入缓存(Clear),具体定义如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class LEVELDB_EXPORT WriteBatch {
  public:	
  //...
  void Put(const Slice& key, const Slice& value);

  void Delete(const Slice& key);

  void Clear();
  //...
};

当我们想将 WriteBatch 中的数据写入 DB 时,只需要调用 Write 接口,其主要用于处理之前保存在 WriteBatch 对象中的所有批量操作,其详细接口定义如下所示:

1
Status Write(const WriteOptions& options, WriteBatch* updates);
  • 注意:一旦我们写入完成后,就会调用 updates 中的 Clear 来清空之前保存的批量操作。

代码示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include"leveldb/db.h"
#include"leveldb/slice.h"
#include"leveldb/write_batch.h"
#include<iostream>

using namespace std;

int main()
{
  leveldb::DB* db;
  leveldb::Options op;
  op.create_if_missing = true;

  //打开数据库
  leveldb::Status status = leveldb::DB::Open(op, "/tmp/testdb", &db);
  if(!status.ok())
  {
      cout << status.ToString() << endl;
      exit(1);
  }

  //批量写入
  leveldb::Slice key;
  string value;
  leveldb::WriteBatch batch;
  for(int i = 0; i < 10; i++)
  {
      value = ('0' + i);
      key = "k" + value;
      batch.Put(key, value);
  }

  status = db->Write(leveldb::WriteOptions(), &batch);
  if(status.ok())
  {
      cout << "批量写入成功" << endl;   
  }

  //批量删除
  for(int i = 0; i < 10; i++)
  {
      key = "k" + ('0' + i);
      batch.Delete(key);
  }

  status = db->Write(leveldb::WriteOptions(), &batch);
  if(status.ok())
  {
      cout << "批量删除成功" << endl;   
  }

  //关闭数据库
  delete db;

  return 0;
}

迭代器遍历

针对 DB 中所有的数据记录,LevelDB 不仅支持前向的遍历,也支持反向的遍历。在 DB 对象类型中,通过调用NewIterator 创建一个新的迭代器对象,该接口具体定义如下:

1
Iterator* NewIterator(const ReadOptions&);
  • ReadOptions& option:用于指定在遍历访问过程中的相关设置。

这里有一个需要注意的地方,这里返回的迭代器不能直接使用,而是需要先使用对应的 Seek 操作偏移到指定位置后才能进行对应的迭代操作。

代码示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include"leveldb/db.h"
#include"leveldb/slice.h"
#include"leveldb/iterator.h"
#include<iostream>

using namespace std;

int main()
{
  leveldb::DB* db;
  leveldb::Options op;
  op.create_if_missing = true;

  //打开数据库
  leveldb::Status status = leveldb::DB::Open(op, "/tmp/testdb", &db);
  if(!status.ok())
  {
      cout << status.ToString() << endl;
      exit(1);
  }

  leveldb::Iterator* it = db->NewIterator(leveldb::ReadOptions());

  //正向遍历
  cout << "开始正向遍历:" << endl;
  for(it->SeekToFirst(); it->Valid(); it->Next())
  {
      cout << "key: " << it->key().ToString() << " value: " << it->value().ToString() << endl;
  }

  //逆向遍历
  cout << "开始逆向遍历:" << endl;
  for(it->SeekToLast(); it->Valid(); it->Prev())
  {
      cout << "key: " << it->key().ToString() << " value: " << it->value().ToString() << endl;
  }

  //范围查询
  cout << "开始范围查询:" << endl;
  for(it->Seek("k4"); it->Valid() && it->key().ToString() < "k8"; it->Next())
  {
      cout << "key: " << it->key().ToString() << " value: " << it->value().ToString() << endl;
  }

  delete it;

  //关闭数据库
  delete db;

  return 0;
}

常用优化方案

压缩

当每一个块写入存储设备中时,可以选择是否对块进行压缩后再存储,以及 Options 参数的 compression 成员变量决定是否开启压缩。默认情况下,压缩是开启的,且压缩的速度很快,基本对整体的性能没有太大影响

用户在调用时,可以用 kNoCompression 或 kSnappyCompression 对 compression 参数进行设定,从而确定块在实际存储过程中是否进行压缩。

1
2
3
leveldb::Options op;
op.compression = leveldb::kNoCompression;		//不启用压缩
op.compression = leveldb::kSnappyCompression;	//Snappy压缩

缓存

Cache的作用是充分利用内存空间,减少磁盘的 I/O 操作,从而提升整体运行性能。LevelDB 默认的 Cache 采用的是 LRU 算法,即近期最少使用的数据优先从 Cache 中淘汰,而经常使用的数据驻留在内存,从而实现对需要频繁读取的数据的快速访问。

LevelDB 中定义了一个全局函数 NewLRUCache 用于创建一个 LRUCache。

1
2
3
leveldb::Options op;
op.block_cache = leveldb::NewLRUCache(10 * 1024 * 1024);
//参数主要用于指定LevelDB的块的Cache空间,如果为NULL则默认为8MB

过滤器

由于 LevelDB 中所有的数据均保存在磁盘中,因而一次 Get 的调用,有可能导致多次的磁盘 I/O 操作。为了尽可能减少读过程时磁盘 I/O 的操作次数,LevelDB 采用了 FilterPolicy 机制。LevelDB 中 Options 对象类型的filter_policy 参数,主要用于确定运行过程中 Get 所遵循的 FilterPolicy 机制。

用户可以通过调用 NewBloomFilterPolicy 接口函数以创建布隆过滤器,并将其赋值给对应的 filter_policy 参数。

1
2
leveldb::Options op;
op.filter_policy = leveldb::NewBloomFilterPolicy(10);

命名

LevelDB 中磁盘数据读取与缓存均以块为单位,并且实际存储中所有的数据记录均以 key 进行顺序存储。根据排序结果,相邻的 key 所对应的数据记录一般均会存储在同一个块中。正是由于这一特性,用户针对自身的应用场景需要充分考虑如何优化 key 的命名设计,从而最大限度地提升整体的读取性能。

为了提升性能,命名规则是:**针对需要经常同时访问的数据,其 key 在命名时,可以通过将这些 key 设置相同的前缀保证这些数据的 key 是相邻近的,从而使这些数据可存储在同一个块内。**基于此,那些不常用的数据记录自然会放置在其他块内。

Licensed under CC BY-NC-SA 4.0
最后更新于 May 23, 2022 23:51 CST
Built with Hugo
主题 StackJimmy 设计