• Home
  • About
    • Road to Coding photo

      Road to Coding

      只要那一抹笑容尚存, 我便心无旁骛

    • Learn More
    • Email
    • Github
  • Posts
    • All Posts
    • All Tags

leveldb的入门级使用

04 Mar 2019

Headers

LevelDB本质上只是C/C++的KV存储库, 那么对于一个库, 首先来浏览它的头文件 编译安装之后, 头文件目录会在/usr/include/leveldb下

header description
db.h 核心头文件,提供DB句柄
status.h LevelDB所有操作返回状态的抽象
options.h LevelDB所有操作的选项设置, 尤其包括ReadOption & WriteOption
env.h LevelDB与操作系统底层的基类, 可继承后在Options.env中修改
filter_policy.h LevelDB过滤器的基类, 可继承后在Options.filter_policy中设置
comparator.h KV比较函数, 可进行继承后设置
slice.h LevelDB中Key的默认类型, 是二进制安全的, 类似于SDS
iterator.h LevelDB中进行迭代的迭代器
write_batch.h LevelDB的原子操作设施
table.h 较低层次的Table设置, 一般不用
table.builder.h 同上
cache.h 缓存相关, 除性能调优外一般不用
c.h LevelDB的C接口
dumpfile.h 进行文件Dump的函数, 仅含DumpFile()

leveldb::Open

对于一款数据库, 我们开始使用, 就要从打开数据库开始. leveldb的打开十分简单, 我们先上代码

#include <cassert>
#include <leveldb/db.h>
#include <leveldb/status.h>

leveldb::DB *db;                              // 数据库操作句柄
leveldb::Options options;                     // 设置选项
options.create_if_missing = true;             // 不存在的情况下创建
leveldb::Status status = leveldb::Open(options, "/path/to/db", &db);    // Open操作
assert(status.ok());                          // 断言操作后的状态

上面的代码十分简明清晰. 我们打开一个数据库可以通过这样的步骤:

  1. 创建句柄
  2. 设置选项
  3. 打开数据库
  4. 断言 (非必须, 可以用)

至此, 我们之后都可以通过 db 这个数据库句柄进行数据库的操作

leveldb::Options

leveldb::Options是与数据库的打开设置息息相关的. 我们可以从源码中窥知一二

// leveldb/src/include/options.h

struct Options {
  bool create_if_missing;        // 类似于 O_CREATE 选项 default: false
  bool error_if_exists;          // 与上面的选项同时设置会报错 default: false
  bool paranoid_checks;
  Env* env;                      // leveldb与操作系统底层的交互, default: Env::default()
  Logger* info_log;              // 设置日志级别
  size_t write_buffer_size;      // 设置写缓冲区大小, 可用来调优 defualt: 4096KB
  int max_open_files;            // 最大打开文件数, 可用来调优  default: 1000
  Cache* block_cache;            // 设置Cache, 可以明显提高性能 defualt: false
  size_t block_size;             // 设置block块大小 defualt: 4MB
  int block_restart_interval;
  CompressionType compression;   // 使用的压缩算法, 可以配合Google开源压缩算法使用
  const FilterPolicy* filter_policy;   // 设置过滤器, 如Bloom Filter, default: NULL
  Options();                      // 默认构造函数
};

我们每次在打开数据库前, 可以通过特定的leveldb::Options进行数据库的设置, 从而进行调优 如果, 不进行设置, 可以直接使用临时对象, 使用默认配置即可. 如: auto status = leveldb::Open(leveldb::Options(), "path/to/db/", &db);

leveldb::Status

众所周知, Google的编码规范中, 不建议使用异常. Google C++ 编码规范

但是, 去使用UNIX/WIN Syscall时的错误码吗? 又显得太过粗糙

leveldb提供了leveldb::Status进行状态检测, 如下:

// leveldb/src/include/status.h

class Status {
 public:
  C-tor ...
  
  Status getter ...
  
  // 判断状态的各类函数
  bool ok() const { return (state_ == NULL); }
  bool IsNotFound() const { return code() == kNotFound; }
  bool IsCorruption() const { return code() == kCorruption; }
  bool IsIOError() const { return code() == kIOError; }

  // if(!status.ok()), 可以输出错误, 类似于std::exception::what()
  std::string ToString() const;

 private:
  // 保存状态的是字符串, state_[0..3]为长度, state_[4]为Code, state_[5...]为message
  const char* state_;

  // 设置的状态位, 1, 2, 3, 4, 5
  enum Code {
    kOk = 0,
    kNotFound = 1,
    kCorruption = 2,
    kNotSupported = 3,
    kInvalidArgument = 4,
    kIOError = 5
  };

  // 获取Code
  Code code() const {
    return (state_ == NULL) ? kOk : static_cast<Code>(state_[4]);
  }

  ...
};

使用leveldb::Status可以方便我们进行状态判别, 方式值得学习

leveldb::Close

那么,如何关闭leveldb的数据库呢? 上面已经提到句柄这个词了.

很简单 delete db;即可.

问题来了, 为什么不使用RAII呢? 个人推测有这方面的原因:

  1. 作者个人编码风格影响
  2. Google手册中对新特性一般
  3. 正经的原因: 暴露句柄也容易操作, 而且leveldb是底层的KV库, 使用这可以自行封装, 即使暴露原生指针也是可以的

leveldb::Put/Get/Delete

说到最关键的地方了, 作为KV库, 最本质的功能来了: 存取, 修改, 删除功能 是下面的用法:

//interface

Status Put(const WriteOptions& options, const Slice& key, const Slice& value);
Status Get(cosnt ReadOptions& options, const Slice& key, std::string *value);
Status Delte(const WriteOptions& options, const Slice& key);
#include <leveldb/db.h>

... open a database

const slice/std::string key(...);
std::string value;
auto status = db->Put(leveldb::WriteOptions(), key, value);
if (status.ok()) status = db->Get(leveldb::ReadOptions(), key, &value);
if (status.ok()) status = db->Delete(leveldb::WriteOptions(), key);

如上使用即可, 接口简单易用, 风格良好 在这部分中, 我们有遇到了两个新的Options, leveldb::ReadOptions, leveldb::WriteOptions 使用方法,同leveldb::Options, 进行设置, 使用即可, 默认设置使用临时对象即可.

Q: 为什么DELETE, 也是使用leveldb::WriteOptions呢? A: 实际上, PUT, DELETE操作仅仅只是置位的区别, 其他别无二致

leveldb::WriteBatch

我们在进行数据库的使用中, 有事务的概念, 事务具有原子性. 我们在进行某些操作的时候, 必须需要原子性, 那么, 原子性如何实现 ?

leveldb::WriteBatch实现了原子性操作的控制

#include <leveldb/db.h>
#include <leveldb/write_batch.h>
#include <leveldb/options.h>

open a database...

auto status = db->Put(leveldb::WriteOptions(), key, value);
if (status.ok()) {
    leveldb::WriteBatch batch;
    batch.Delete(key);
    batch.Put(leveldb::WriteOptions(), key_, value_);
    status = db->Write(leveldb::WriteOptions(), &batch);
}

我们可以理解为将操作缓存在leveldb::WriteBatch中, 之后一次性原子操作.

WriteBatch除了有原子性, 还具有批量性操作加速的特点

leveldb::Iterator

因为数据库中存储的内容相当多, 所以我们必须有迭代措施. leveldb::Iterator便是为此而生

// leveldb::Iterator

bool Valid() const;                   # 判断是否合法, 控制遍历条件
void SeekToFirst();                   # STL::Begin()
void SeekToLast()                     # STL::End()
void Seek(const Slice& target);       # 可以跳转到指定位置
void Next();                          # 后向移动, STL::Iterator++
void Prev();                          # 前向移动, STL::Iterator--
Slice key() const;                    # 返回节点Key
Slice value() const;                  # 返回节点Value
Status status() const;                # 返回节点Status

使用示例:

auto iter = db->NewIterator(leveldb::ReadOptions());                 // (1)
for (iter->SeekToFirst(); iter->Valid(); iter->Next())
    std::cout << iter->key().ToString() << " " << iter->value().ToString() << std::endl;

// [start, limit)
auto iter = db->NewIterator(leveldb::ReadOptions());                 // (2)
for (iter->Seek(key) && iter->key() < limit; 
     iter->Valid(); 
     iter->Next())
    ...

auto iter = db->NewIterator(leveldb::ReadOptions());                 // (3)
for (iter->SeekToLast(); iter->Valid(); iter->Prev())
    ...

上面分别展示了 (1)正向遍历, (2)指定位置遍历, (3)反向遍历 注意: 反向遍历要比正向遍历慢 !

leveldb::Snapshot

因为数据库实时在变动, 我们可能只需要其某一个时刻的状态即可, 快照机制是必须的. 当使用快照机制的时候, 所有操作限定在目标版本上操作

leveldb::ReadOptions read_options;
read_options.snapshot = db->GetSnapshot();
...
...

db->ReleaseSnapshot(oread_options.snapshot);

其中我们要注意, 在长期不使用快照的时候, leveldb::DB::ReleaseSnapshot()释放快照 避免长期维护不使用的状态数据.

leveldb::Slice

至此, 我们已经在很多接口中见过 leveldb::Slice了, 这个数据结构是leveldb中维护的特定串数据结构 某种意义上, 类似于SDS (Redis中的简单字符串) 其本质是, 一个length和外部字符串, 因此开销很低 (试试 std::move ? )

class LEVELDB_EXPORT Slice {
 public:
  ........ // interface
 private:
  const char* data_;
  size_t size_;
};

对于leveldb::Slice支持和std::string的无缝转换 , (使用leveldb::Slice::Constructor + leveldb::Slice::TOStirng())

使用Slice有一个很大的误区!

因为其使用的是外部字符串, 所以需要使用者, 自行保证生命期, 尤其是在if-case/while中

{
    Slice slice;
    if (...) {
        slice = std::string("key");        // WARRRRRRRR !
	}
    db->Get(leveldb::ReadOptions(), slice, &val);
}

Slice 藉由 std::string 这样实现

// Create a slice that refers to the contents of "s"
  Slice(const std::string& s) : data_(s.data()), size_(s.size()) { }

所以在跳出作用域之后, std::string析构, Slice也会受到影响 !

level::Comparator

众所周知, 作为key-value存储, 我们需要指定比较器, 比如:std::map 但是一旦我们的K-V不单纯, 有自行定义的类型时, 我们就需要自定义比较器, 用来满足KV的绝对弱序.

定义方法嘛, 和platinum还是挺类似的

继承基类接口, 自定义实现即可 (但是platinum实现欠佳, 目前未统一接口, 不过思想到位了)

#include <leveldb/comparator.h>

class OtherComparator : public leveldb::Comparator {

# 最核心的比较器接口
virtual int Compare(const Slice& a, const Slice& b) const = 0;

# 下面三个用来实现向后兼容性
virtual const char* Name() const = 0;
virtual void FindShortestSeparator(
      std::string* start,
      const Slice& limit) const = 0;
virtual void FindShortSuccessor(std::string* key) const = 0;
}

实现之后, leveldb::Options, 懂我意思了吧

#include <leveldb/db.h>

leveldb::DB *db;
leveldb::Options options;
OthreComparator cmp;
options.comparator = &cmp;

leveldb::Open(options, "path/to/db", &db);

leveldb::FilterPolicy

过滤器是KV中经常使用的设施, 比如Google-BigTable中使用了 Bloom Filter

使用过滤器有什么用呢? 可以加速查询, 减少硬盘查询的次数

在这一点上, Redis很快, 因为是基于内存的KV, 但是leveldb占用空间极小, 各有优劣

那么, 如何做, 同leveldb::Comparator

// 实现以下接口即可

class OthreFileterPolicy : public leveldb::FiuleterPolicy {
 public:
  virtual const char* Name() const = 0;

  virtual void CreateFilter(const Slice* keys, int n, std::string* dst)
      const = 0;

  virtual bool KeyMayMatch(const Slice& key, const Slice& filter) const = 0;
};

}

之后, 使用方法老套路

leveldb::DB *db;
leveldb::Options options;
OthreFileterPolicy filter;
options.filter_policy = &filter;

leveldb::Open(options, "path/to/db", &db);

leveldb::Env

同理, Env所管理的是与OS交互的文件操作. 我们可以认为设置行为模式, 同上理:

class OtherEnv : public leveldb::Env {
    .. implementation of the Env interface ...
  };

OtherEnv env;
leveldb::Options options;
options.env = &env;
Status s = leveldb::DB::Open(options, ...);

Performance

我们同时可以进行性能调整.有下面几个选项, 这几个选项都是可以在leveldb::Options中设置的

leveldb::Options options;

options.block_size = xxx;                             # 设置block size
options.compression = leveldb::KNocompression;        # 设置压缩方式
options.cache = leveldb::NewLRUCache(100 * 1048576);  # 设置缓存
options.filter_policy = xxxxxx;                       # 设置过滤器

这是节个基础的调整性能设施的方法. 其他调优就可能需要从内核层面上入手了

Concurrency

对于一款数据库而言, 并发性的处理十分关键, 其直接关系的数据库性能 对于leveldb而言.一个数据库在同一时间内只能由一个进程打开, leveldb通过从操作系统中获取锁的方式防止误用。

在单个进程中,同一个leveldb::DB对象可以安全的由多个并发线程共享使用。

即,不同的线程可以同时写入或获取迭代器,

或在没有任何外部同步的情况在同一数据库上调用leveldb::DB::Get

但是其他对象(如leveldb::Iterator和leveldb::WriteBatch)可能需要外部同步。

如果两个线程共享这样的对象,它们必须使用自己的协议锁来保护自己的访问。(自行同步)

Sync Writes

这里有一个关键的内容: leveldb的写操作默认都是异步的 在将操作推送到操作系统底层之后, write操作便会返回.

那么, 存在一个隐患: Write返回之后, 宕机, 所有异步操作失效

我们可以这样操作:

leveldb::WriteOptions options;
options.sync = true;

db->Put(options, ...);

即可实现同步写入, 但是异步操作的性能十分高啊! ! !

取下面这样两种折中方案:

  1. 设有P次操作, 将其中N(0 < N <= M), M(N< M <= P)次操作使用同步手法写, 这样可以保证只有部分失效, 重启后,可以使用CURRENT文件进行恢复
  2. 借用leveldb::WriteBatch, 在db->Write()中options.sync = true, 本次的同步开销将在每次操作中均摊

以上两种方法,均能获得还不错的性能

参考资料

leveldb中文网doc leveldb GitHub leveldb源码commits


leveldb代码十分精致 (真的像艺术品), 十分推荐阅读! 个人在找暑期实习, 时间可能会拖一点, 不过不会迟到! 另外SSDB也是挺有意思的项目, 我自己的项目也在做一个类似的, 不过可能就没有SSDB那么丰富,

那么, 下次leveldb源码阅读再见, 或者是 LevelNet架构解析再见了



LevelDB Share Tweet +1