LevelDB 浅谈3--WriteBatch

1,399 阅读4分钟

WriteBatch 类,从它的名字就可以看出,该类的作用就是批量写入数据,将数据存储到指定的Memtable中。

那么,同学们就要思考,用户批量写入的 key-value 如何存储在 WriteBatch 对象中的呢?WriteBatch 又在什么时候,以什么方式将 批量的 key-value 插入到MemTable中呢?

class LEVELDB_EXPORT WriteBatch {
 public:
  class LEVELDB_EXPORT Handler {
   public:
    virtual ~Handler();
    virtual void Put(const Slice& key, const Slice& value) = 0;
    virtual void Delete(const Slice& key) = 0;
  };

  WriteBatch();
  WriteBatch(const WriteBatch&) = default;
  WriteBatch& operator=(const WriteBatch&) = default;

  ~WriteBatch();

  // 新增 key-value
  void Put(const Slice& key, const Slice& value);

  // If the database contains a mapping for "key", erase it.  Else do nothing.
  void Delete(const Slice& key);
  void Clear();
  size_t ApproximateSize() const;
  void Append(const WriteBatch& source);
  Status Iterate(Handler* handler) const;

 private:
  friend class WriteBatchInternal;
  std::string rep_;  // See comment in write_batch.cc for the format of rep_
};

void WriteBatch::Put(const Slice& key, const Slice& value) {
  WriteBatchInternal::SetCount(this, WriteBatchInternal::Count(this) + 1);
  rep_.push_back(static_cast<char>(kTypeValue));
  PutLengthPrefixedSlice(&rep_, key);
  PutLengthPrefixedSlice(&rep_, value);
}

void WriteBatch::Delete(const Slice& key) {
  WriteBatchInternal::SetCount(this, WriteBatchInternal::Count(this) + 1);
  rep_.push_back(static_cast<char>(kTypeDeletion));
  PutLengthPrefixedSlice(&rep_, key);
}

void WriteBatch::Append(const WriteBatch& source) {
  WriteBatchInternal::Append(this, &source);
}

成员对象 std::string rep_ ,用来存储批量插入的key-value,具体结构如下:

image.png

sequence 是一个64位的序列号,req_的唯一标识;

count 32位,标识该rep_存储了多少key-value,用 EncodeFixed32 对int32进行硬编码。

record 字节长度不确定,用来存储key-value。

image.png

key size 是对 key.size()进行 VarInt 编码,value size 同理。

WriteBatch 类的接口就很容易理解,这里就不做讲解,从接口的输入可以看出,WriteBatch 类的接口都是对外提供,用来直接处理 key-value 和 其他 WriteBatch 对象的。

实际操作 rep_ 的函数都存储在 WriteBatchInternal 类中。WriteBatchInternal 作为 WriteBatch 的友元类,这样的代码结构,目的为了函数结构看起来更加整洁。即将操作 rep_ 成员数据的一些函数放到类 WriteBatchInternal 中。

class WriteBatchInternal {
 public:
  // 获取 rep_ 中 key-value 的个数
  static int Count(const WriteBatch* batch);
  // 这只 rep_ 中 key-value 的个数
  static void SetCount(WriteBatch* batch, int n);

  // 获取 WriteBatch 对象的 rep_ 的序列号
  static SequenceNumber Sequence(const WriteBatch* batch);
  // 设置 WriteBatch 对象的 rep_ 的序列号
  static void SetSequence(WriteBatch* batch, SequenceNumber seq);
  
  // 获取 WriteBatch 对象的 rep_ 的全部内容
  static Slice Contents(const WriteBatch* batch) { return Slice(batch->rep_); }
  // // 获取 WriteBatch 对象的 rep_ 的大小
  static size_t ByteSize(const WriteBatch* batch) { return batch->rep_.size(); }
  // 设置 WriteBatch 对象的 rep_ 的全部内容
  static void SetContents(WriteBatch* batch, const Slice& contents);

  static Status InsertInto(const WriteBatch* batch, MemTable* memtable);

  static void Append(WriteBatch* dst, const WriteBatch* src);
};

Status WriteBatchInternal::InsertInto(const WriteBatch* b, MemTable* memtable) {
  MemTableInserter inserter;
  inserter.sequence_ = WriteBatchInternal::Sequence(b);
  inserter.mem_ = memtable;
  return b->Iterate(&inserter);
}

接口 WriteBatchInternal::InsertInto 作用就是将 输入的 WriteBatch 对象维护的批量key-value 数据插入到指定的 Memtable 对象中。

MemTableInserter

在 WriteBatch 将批量 key-value 对插入到 MemTable 中时,需要借助类 MemTableInserter。 首先类 MemTableInserter 继承纯虚基类 Handler,而 Handler 类作为 WriteBatch 的内嵌类。同学们首先要明白一点,内嵌类存在的意义是做为外部类(WriteBatch)功能实现的底层实现。

MemTableInserter 类存在的意义,就是为了实现 WriteBatch 批量插入 key-value 到 MemTable 中

namespace {
class MemTableInserter : public WriteBatch::Handler {
 public:
  SequenceNumber sequence_;
  MemTable* mem_;

  void Put(const Slice& key, const Slice& value) override {
    mem_->Add(sequence_, kTypeValue, key, value);
    sequence_++;
  }
  void Delete(const Slice& key) override {
    mem_->Add(sequence_, kTypeDeletion, key, Slice());
    sequence_++;
  }
};

MemTableInserter 类作为一个第三方中介人,记录本次交易ID(sequence) 和 目的 MemTable对象,将 WriteBatch 交出的key-value 按指定方式(新增/删除)交接该 Memtable 对象。

Status WriteBatch::Iterate(Handler* handler) const {
  Slice input(rep_);
  if (input.size() < kHeader) {
    return Status::Corruption("malformed WriteBatch (too small)");
  }

  input.remove_prefix(kHeader);
  Slice key, value;
  int found = 0;
  while (!input.empty()) {
    found++;
    char tag = input[0];
    input.remove_prefix(1);
    switch (tag) {
      case kTypeValue:
        if (GetLengthPrefixedSlice(&input, &key) &&
            GetLengthPrefixedSlice(&input, &value)) {
          handler->Put(key, value);
        } else {
          return Status::Corruption("bad WriteBatch Put");
        }
        break;
      case kTypeDeletion:
        if (GetLengthPrefixedSlice(&input, &key)) {
          handler->Delete(key);
        } else {
          return Status::Corruption("bad WriteBatch Delete");
        }
        break;
      default:
        return Status::Corruption("unknown WriteBatch tag");
    }
  }
  if (found != WriteBatchInternal::Count(this)) {
    return Status::Corruption("WriteBatch has wrong count");
  } else {
    return Status::OK();
  }
}

WriteBatch 类自身实现了一个类似迭代器的函数,即将其对象内部维护的 key-value 一个一个地取出来,按照指定方式(新增/删除)交接给 MemTableInserter 对象。

总结

同学们阅读了上面的介绍,对 levelDB 中批量数据的插入有了大概的了解。其实还有两个问题需要考虑:

  1. LevelDB 本身是多线程的,如果保证批量操作 key-value 呢?
  2. 什么时候用到 WriteBatch 类对象进行批量插入;
  3. 使用 WriteBatch 类对象进行批量插入,数据实际暂时存储在 WriteBatch 对象的 rep_ 数据成员中。那么什么时候,调用 WriteBatchInternal::InsertInto ,将 rep_ 中的 key-value 插入到 MemTable 中呢?

上面两个问题,下回分晓。