levelDB 代码阅读笔记 01 db.h

Overview

背景

最近在做一个智能算力相关的项目,类似美团外卖广告智能算力的探索与实践 其中实现控制系统需要与数据库交互。 虽然最后技术选型并没有使用到levelDB,但是想趁机把代码读了吧。

很惊讶的发现我大三的时候声称自己读过部分levelDB代码,甚至还写了几篇相关的博客,比如

但是我却一点都没印象了.... 仔细看来很多概念在当时可能都是没有充分理解的,而且从数目上来看,应该并没有完整看完levelDB代码。

所以重新开个坑,看看自己比起毕业前有没有长进【没有

先从入口 include/leveldb/db.h 开始

LEVELDB_EXPORT

看到LEVELDB_EXPORT这个macro

1
2class LEVELDB_EXPORT Snapshot {
3 protected:
4  virtual ~Snapshot();
5};
6

是在 include/leveldb/export.h 中定义的

 1
 2// 符号可见性问题,使用macro来控制在编译成动态库时暴露,在Link时不暴露符号是一种common 的做法
 3// 
 4#if !defined(LEVELDB_EXPORT)
 5
 6#if defined(LEVELDB_SHARED_LIBRARY)
 7#if defined(_WIN32)
 8
 9#if defined(LEVELDB_COMPILE_LIBRARY)
10#define LEVELDB_EXPORT __declspec(dllexport)
11#else
12#define LEVELDB_EXPORT __declspec(dllimport)
13#endif  // defined(LEVELDB_COMPILE_LIBRARY)
14
15#else  // defined(_WIN32)
16#if defined(LEVELDB_COMPILE_LIBRARY)
17#define LEVELDB_EXPORT __attribute__((visibility("default")))
18#else
19#define LEVELDB_EXPORT
20#endif
21#endif  // defined(_WIN32)
22
23#else  // defined(LEVELDB_SHARED_LIBRARY)
24#define LEVELDB_EXPORT
25#endif
26
27#endif  // !defined(LEVELDB_EXPORT)
28
29#endif  // STORAGE_LEVELDB_INCLUDE_EXPORT_H_
30
31

通常在编译为动态链接库时,需要将某些符号设置为可见; 在link 这些动态库时,将这些符号设置为hidden

because the same header file is generally used both when compiling the DLL and in client code that consumes the DLL's interface, it is a common pattern to define a macro that automatically resolves to the appropriate attribute specifier at compile-time

可以参考

slice

代码位于include/leveldb/slice.h 可以参考 这里的说明

Slice 基本就是一个简单版本的std::string_view,提供一个只读的窗口

与std::string_view相似, Slice的生命周期依赖于外部数据的生命周期

值得一提的可能是Slice::compare 函数,用来做一个三路比较

 1
 2
 3inline int Slice::compare(const Slice& b) const {
 4 const size_t min_len = (size_ < b.size_) ? size_ : b.size_;
 5 int r = memcmp(data_, b.data_, min_len);
 6 if (r == 0) {
 7   if (size_ < b.size_)
 8     r = -1;
 9   else if (size_ > b.size_)
10     r = +1;
11 }
12 return r;
13}
14

实际上这部分可以借助c++20的default_comparisons 做一个简化

Status

Status Class 是对levelDB operation的结果做了一层封装。

比较巧妙的地方在于,实现中将"状态码,错误信息“全部压缩在了一个char*中.

1
2  // OK status has a null state_.  Otherwise, state_ is a new[] array
3  // of the following form:
4  //    state_[0..3] == length of message
5  //    state_[4]    == code
6  //    state_[5..]  == message
7  const char* state_;
8

这个实现主要是性能会有优势,原因之一是传递参数的时候只需要传一个char*,就可以将code和message全部进行传递。

这样的state_实现决定了copy函数的实现,需要先从前4个byte拿到size,然后再进行copy

1
2const char* Status::CopyState(const char* state) {
3  uint32_t size;
4  std::memcpy(&size, state, sizeof(size));
5  // 5是4byte size + 1byte code
6  char* result = new char[size + 5];
7  std::memcpy(result, state, size + 5);
8  return result;
9}

此外,一些静态函数的声明最初有些让人困惑,为啥要有两个Slice? 一个Slice不就够了吗

1  static Status NotFound(const Slice& msg, const Slice& msg2 = Slice()) {
2    return Status(kNotFound, msg, msg2);
3  }
4

看了调用时的使用,发现是在一些场景下,错误信息会有两部分组成。 前一部分是大的类别的错误信息,后面可能是错误码(其他第三方的错误码)

这样实现感觉主要是减轻了调用方的负担,因为不需要在调用测自己做message的拼接了。

 1
 2
 3Status::Status(Code code, const Slice& msg, const Slice& msg2) {
 4  assert(code != kOk);
 5  // 两种情况
 6  // 1: msg2为空,len2为0, 返回的结果由msg决定
 7  // 2: msg2不为空, size为 size(msg1) + len(": ") + size(msg2). 中间的2是分隔符
 8  const uint32_t len1 = static_cast<uint32_t>(msg.size());
 9  const uint32_t len2 = static_cast<uint32_t>(msg2.size());
10  const uint32_t size = len1 + (len2 ? (2 + len2) : 0);
11  char* result = new char[size + 5];
12  std::memcpy(result, &size, sizeof(size));
13  result[4] = static_cast<char>(code);
14  std::memcpy(result + 5, msg.data(), len1);
15  if (len2) {
16    result[5 + len1] = ':';
17    result[6 + len1] = ' ';
18    std::memcpy(result + 7 + len1, msg2.data(), len2);
19  }
20  state_ = result;
21}
22
23

注释

include/leveldb/db.h 文件算是整个项目的接口,注释写的全面而详细

大概有以下几个方面:

  • 函数是做什么的
  • 正常情况下的返回值;异常情况下的返回值
  • 参数的生命周期
  • 当操作的key不存在时,是否返回错误(如Delete不会返回错误,但似乎Get会返回错误)
  • 线程安全性。 哪些部分是可以直接多个thread调用的,哪些需要外部加锁

Posts in this Series