【leveldb】SSTable(十二):Data Block

DataBlock属于SSTable中Block的一种,关于DataBlock与SSTable的关系,可点此链接查看SSTable结构说明
本篇主要是对DataBlock的读写流程解读。这里为便于理解,则将DataBlock格式再次放出:
在这里插入图片描述

图1

写流程

写流程很简单,就是按照DataBlock格式去封装。

block_builder.h
namespace leveldb {

struct Options;

class BlockBuilder {
 public:
  explicit BlockBuilder(const Options* options);

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

  // Reset the contents as if the BlockBuilder was just constructed.
  void Reset();

  // REQUIRES: Finish() has not been called since the last call to Reset().
  // REQUIRES: key is larger than any previously added key
  void Add(const Slice& key, const Slice& value);

  // Finish building the block and return a slice that refers to the
  // block contents.  The returned slice will remain valid for the
  // lifetime of this builder or until Reset() is called.
  Slice Finish();

  // Returns an estimate of the current (uncompressed) size of the block
  // we are building.
  size_t CurrentSizeEstimate() const;

  // Return true iff no entries have been added since the last Reset()
  bool empty() const { return buffer_.empty(); }

 private:
  <!写KV时的一些参数选项,主要用到了参数:
    1、block_restart_interval(重启点间隔);
    2、comparator(KV比较)。
  >
  const Options* options_;
  std::string buffer_;              // Destination buffer
  std::vector<uint32_t> restarts_;  // Restart points
  <!用于统计插入的KV个数,便于和重启点间隔比较,
    进而决定是不是要新设置一个重启点。
  >
  int counter_;                     // Number of entries emitted since restart
  bool finished_;                   // Has Finish() been called?
  std::string last_key_;
};

}  // namespace leveldb
block_builder.cc
namespace leveldb {
<!构建一个组织DataBlock格式方法类>
BlockBuilder::BlockBuilder(const Options* options)
    : options_(options), restarts_(), counter_(0), finished_(false) {
  assert(options->block_restart_interval >= 1);
  restarts_.push_back(0);  // First restart point is at offset 0
}

<!重置,主要把用于生产DataBlock的一些临时变量重置>
void BlockBuilder::Reset() {
  buffer_.clear();
  restarts_.clear();
  restarts_.push_back(0);  // First restart point is at offset 0
  counter_ = 0;
  finished_ = false;
  last_key_.clear();
}

<!获取DataBlock的大致大小,DataBlock由三部分组成:
  1、存储KV数据的buffer_;
  2、存储重启点的数组restart_[],每个重启点类型占4Byte;
  3、表示重启点个数的num_restarts_,占4Byte。
>
size_t BlockBuilder::CurrentSizeEstimate() const {
  return (buffer_.size() +                       // Raw data buffer
          restarts_.size() * sizeof(uint32_t) +  // Restart array
          sizeof(uint32_t));                     // Restart array length
}

<!表示完成一个DataBlock,这里就是在已存有KV的buffer_尾部追加
  重启点restart[]和表示重启点个数的restart.size()>
Slice BlockBuilder::Finish() {
  // Append restart array
  for (size_t i = 0; i < restarts_.size(); i++) {
    PutFixed32(&buffer_, restarts_[i]);
  }
  PutFixed32(&buffer_, restarts_.size());
  finished_ = true;
  return Slice(buffer_);
}

<!这里就是往DataBlock中添加一个key。
  上层已保证添加的Key都是有序而且是递增的。
>
void BlockBuilder::Add(const Slice& key, const Slice& value) {
  Slice last_key_piece(last_key_);
  assert(!finished_);

  <!这里要说下重启点间隔参数options_->block_restart_interval,
    我们知道DataBlock为压缩空间,先将一个Entry的完整key保存下来,
    后续先添加的Entry的key只保存与前一个key不同的部分。而多少个key之后
    再保存一个完整的key,则就是靠重启点间隔参数来控制的。
  >
  <!两个重启点间隔直接的KV个数conter_肯定是 <= 重启点间隔>
  assert(counter_ <= options_->block_restart_interval);
  
  <!第一次进来,或者后续进来的key都是大于前一个key的>
  assert(buffer_.empty()  // No values yet?
         || options_->comparator->Compare(key, last_key_piece) > 0);
  size_t shared = 0;
  
  <!若存储KV个数还未达到重启点个数要求,
    则找出新Key与上一个Key的相同部分个数,
    用shared来统计
  >
  if (counter_ < options_->block_restart_interval) {
    // See how much sharing to do with previous string
    const size_t min_length = std::min(last_key_piece.size(), key.size());
    while ((shared < min_length) && (last_key_piece[shared] == key[shared])) {
      shared++;
    }
  } else {
    
    <!表示设置一个重启点,其实就是将buffer_中的接下来要存的
      完整key的offset写入到restart_中,BlockBuilder类构造或者
      调用Reset()方法时,已默认往restarts_中存入了重启点偏移位0>
    // Restart compression
    restarts_.push_back(buffer_.size());
    counter_ = 0;
  }

  <!这里求出前后两个key不相同的部分,如果是新设置的重启点,
    shared值为0,也就是说保存一个完整的key。
  >
  const size_t non_shared = key.size() - shared;

  <!下面流程很好懂了,写各个字段值>
  // Add "<shared><non_shared><value_size>" to buffer_
  PutVarint32(&buffer_, shared);
  PutVarint32(&buffer_, non_shared);
  PutVarint32(&buffer_, value.size());

  // Add string delta to buffer_ followed by value
  buffer_.append(key.data() + shared, non_shared);
  buffer_.append(value.data(), value.size());

  <!将当前新写入的key保存为last_key,
    同将counter_++,用于重启点间隔判断。
  >
  // Update state
  last_key_.resize(shared);
  last_key_.append(key.data() + shared, non_shared);
  assert(Slice(last_key_) == key);
  counter_++;
}

}  // namespace leveldb

读流程

读流程其实就是去按DataBlock格式去解析DataBlock。

block.h
namespace leveldb {

struct BlockContents;
class Comparator;

class Block {
 public:
  <!BlockContents就是DataBlock>
  // Initialize the block with the specified contents.
  explicit Block(const BlockContents& contents);

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

  ~Block();

  size_t size() const { return size_; }
  Iterator* NewIterator(const Comparator* comparator);

 private:
  class Iter;

  uint32_t NumRestarts() const;

  const char* data_;
  <!size_就是DataBlock的大小>
  size_t size_;
  <!重启点数组在DataBlock中起始偏移位>
  uint32_t restart_offset_;  // Offset in data_ of restart array
  <!表示Block是否管理此内存,
   如果为true,则Block自己去释放内存。
  >
  bool owned_;               // Block owns data_[]
};

}  // namespace leveldb
block.cc
namespace leveldb {

<!data_是block首地址,size_是block的大小。
 一个block由block-data、block-restart、block-numRestart
 三部分组成,numRestart占用4个字节,所以这里要求出numRestart
 个数,就是直接解码最后是个字节。
 >

inline uint32_t Block::NumRestarts() const {
  
  <!numRestart是uint32_t表示,所以至少这么大>
  assert(size_ >= sizeof(uint32_t)); 
  return DecodeFixed32(data_ + size_ - sizeof(uint32_t));
}

<!
 这里的BlockContents中的data就是由BlockBuilder生成的Block格式数据。
 heap_allocated表示是否由使用BlockContents的调用者来释放内存。
 >
Block::Block(const BlockContents& contents)
    : data_(contents.data.data()),
      size_(contents.data.size()),
      owned_(contents.heap_allocated) {
      
  <!1、numRestart都是一个uint32_t大小了,所以size_ 都小于一个4字节单元那就是异常了,命size_ = 0>
  if (size_ < sizeof(uint32_t)) {
    size_ = 0;  // Error marker
  } else {
  
    <!2、每个restart[]都是一个uint32_t类型,减去一个uint32_t大小的numRestart大小,估算出restart[]最大个数,如果已存在的numRestart都大于这个
    最大值,则认为是异常的,命size_ 为0>
    size_t max_restarts_allowed = (size_ - sizeof(uint32_t)) / sizeof(uint32_t);
    if (NumRestarts() > max_restarts_allowed) {
      // The size is too small for NumRestarts()
      size_ = 0;
    } else {
      
      <!计算出restart[]在整个block中的偏移位offset.>
      restart_offset_ = size_ - (1 + NumRestarts()) * sizeof(uint32_t);
    }
  }
}

<!析构时判断下是否要删除Block>
Block::~Block() {
  if (owned_) {
    delete[] data_;
  }
}

// Helper routine: decode the next block entry starting at "p",
// storing the number of shared key bytes, non_shared key bytes,
// and the length of the value in "*shared", "*non_shared", and
// "*value_length", respectively.  Will not dereference past "limit".
//
// If any errors are detected, returns nullptr.  Otherwise, returns a
// pointer to the key delta (just past the three decoded values).
<!先看一个Entry的结构:
_____________________________________________________________
| *shared | *non_shared | *value_length | key_delta | value |
_____________________________________________________________
其中*shared、*non_shared、*value_length这三个都是varint32编码,
每个字段最少占用一个字节。
由上面的结构说明之后,看下面的解析很清楚了。
>
static inline const char* DecodeEntry(const char* p, const char* limit,
                                      uint32_t* shared, uint32_t* non_shared,
                                      uint32_t* value_length) {
  if (limit - p < 3) return nullptr;
  *shared = reinterpret_cast<const uint8_t*>(p)[0];
  *non_shared = reinterpret_cast<const uint8_t*>(p)[1];
  *value_length = reinterpret_cast<const uint8_t*>(p)[2];
  if ((*shared | *non_shared | *value_length) < 128) {
    // Fast path: all three values are encoded in one byte each
    p += 3;
  } else {
    if ((p = GetVarint32Ptr(p, limit, shared)) == nullptr) return nullptr;
    if ((p = GetVarint32Ptr(p, limit, non_shared)) == nullptr) return nullptr;
    if ((p = GetVarint32Ptr(p, limit, value_length)) == nullptr) return nullptr;
  }

  <!p此时指向key_delta,limit指向重启点的起始处,所有二者的距离
    至少能容下一个key_delta + Value的大小。否则异常。
  >
  if (static_cast<uint32_t>(limit - p) < (*non_shared + *value_length)) {
    return nullptr;
  }
  return p;
}

<!这个创建一个遍历Block的迭代器,Block中每个K-V就是一个Entry>
class Block::Iter : public Iterator {
 private:
 
  <!KV比较器>
  const Comparator* const comparator_;
  <!Block首地址>
  const char* const data_;       // underlying block contents
  <!restart[]数组在Block中的偏移首地址>
  uint32_t const restarts_;      // Offset of restart array (list of fixed32)
  <!restart点的个数>
  uint32_t const num_restarts_;  // Number of uint32_t entries in restart array

  // current_ is offset in data_ of current entry.  >= restarts_ if !Valid
  <!当前指向的Entry在Block中的偏移>
  uint32_t current_;
  <!表示当前restart所在restart[]中的index>
  uint32_t restart_index_;  // Index of restart block in which current_ falls
  <!当前current_对应的KV>
  std::string key_;
  <!这个Value有多个用途,
   1、当前key对应的value,
   2、指向当前Entry的一个指针
   >
  Slice value_;
  <!操作状态记录>
  Status status_;
  
  <!比较两个K大小,a > b则返回>0, a<b则返回<0, a=b则返回=0>
  inline int Compare(const Slice& a, const Slice& b) const {
    return comparator_->Compare(a, b);
  }

  <!指向下一个Entry>
  // Return the offset in data_ just past the end of the current entry.
  inline uint32_t NextEntryOffset() const {
    return (value_.data() + value_.size()) - data_;
  }

  <!解码返回索引为index的restart的值>
  uint32_t GetRestartPoint(uint32_t index) {
    assert(index < num_restarts_);
    return DecodeFixed32(data_ + restarts_ + index * sizeof(uint32_t));
  }
  
  <!跳到index对应的restart所指向的值,同时要将当前key和value都置位>
  void SeekToRestartPoint(uint32_t index) {
    key_.clear();
    restart_index_ = index;
    // current_ will be fixed by ParseNextKey();

    // ParseNextKey() starts at the end of value_, so set value_ accordingly
    uint32_t offset = GetRestartPoint(index);
    value_ = Slice(data_ + offset, 0);
  }

 public:
  <!构造一个迭代器:
   1、指定比对器comparator_;
   2、指定迭代器所指向的基地址data_;
   3、Block中重启点的偏移restarts_;
   4、重启点的个数num_restarts_;
   5、当前key在Block中的偏移默认为restart_;
   6、当前重启点的索引默认为最后一个即num_restarts_。
  >
  Iter(const Comparator* comparator, const char* data, uint32_t restarts,
       uint32_t num_restarts)
      : comparator_(comparator),
        data_(data),
        restarts_(restarts),
        num_restarts_(num_restarts),
        current_(restarts_),
        restart_index_(num_restarts_) {
    assert(num_restarts_ > 0);
  }
  
  <!当前key的偏移current_要小于重启点的偏移restarts_才是合法的,
   虽然构造迭代器Iter的时候赋值为相等,当后续使用时应该会处理。
   >
  bool Valid() const override { return current_ < restarts_; }
  Status status() const override { return status_; }
  
  <!获取当前key,当要进行合法性判断>
  Slice key() const override {
    assert(Valid());
    return key_;
  }
  
  <!Value值>
  Slice value() const override {
    assert(Valid());
    return value_;
  }
 
  <!Next key,由ParseNextKey负责实现>
  void Next() override {
    assert(Valid());
    ParseNextKey();
  }
  
  <!指向前一个节点,大体思路如下:
   1、先找到一个重启点,这个重启点指向的entry对应的offset小于当前的key的偏移current_。
   2、跳到重启点指向的Entry。
   3、然后在当前指向的entry循环向下遍历,找到当前current_前一个偏移Entry。
   >
  void Prev() override {
    assert(Valid());


    <!循环查找小于当前current偏移的重启点>
    // Scan backwards to a restart point before current_
    const uint32_t original = current_;
    while (GetRestartPoint(restart_index_) >= original) {
      <!如果重启点指向的entry还是>=当前的Entry偏移,
        同时重启点的索引已经为0,表明前面以无Entry,
        当前Entry就是第一个Entry,这里就把current_和
        restart_index重新置位。
      >
      if (restart_index_ == 0) {
        // No more entries
        current_ = restarts_;
        restart_index_ = num_restarts_;
        return;
      }
      restart_index_--;
    }

    SeekToRestartPoint(restart_index_);
    
    <!这里就是循环解析,直到找到当前Entry的前一个Entry,
      ParseNextKey解析出下一个Key-Value,NextEntryOffset在
      已解析出来的KV基础上通过Value addr + Value Size来进一步
      判断下一个Entry是否就是original所指向的entry。
    >
    do {
      // Loop until end of current entry hits the start of original entry
    } while (ParseNextKey() && NextEntryOffset() < original);
  }
  
  <!Seek到Block中大于等于target的Entry,实现方式如下:
   1、先通过二分法在Block的restart[]数组中找到最近的
      一个restart重启点,这个重启点指向的key < target。
   2、然后从这个restart重启点指向的key向下遍历,找到
      大于等于target的Entry。      
  >
  void Seek(const Slice& target) override {
    // Binary search in restart array to find the last restart point
    // with a key < target
    uint32_t left = 0;
    uint32_t right = num_restarts_ - 1;
    
    <!这里判断left >= right就跳出循环,
      然后沿着left指向的值往下寻找。
      >
    while (left < right) {
      uint32_t mid = (left + right + 1) / 2;
      uint32_t region_offset = GetRestartPoint(mid);
      uint32_t shared, non_shared, value_length;
      
      <!解码出restart指向的Entry的key值,且值不为空,
        另外restart指向的key值是不共享的,也就是说shared值为0,
        所以如果二者都不满足,则报错。
       >
      const char* key_ptr =
          DecodeEntry(data_ + region_offset, data_ + restarts_, &shared,
                      &non_shared, &value_length);
      if (key_ptr == nullptr || (shared != 0)) {
        CorruptionError();
        return;
      }
      
      <!
        1、如果解码出的restart指向的key值小于target,但这个restart
           接下来的数据是有可能>=target的,所以left = mid,然后继续查找。
        2、如果解码出的restart指向的key值大于等于target,那这个restart
           后续的的值肯定都是大于等于target的,所以就直接跳过这个mid指向
           的值,将mid - 1 赋值与rigth,然后继续查找。
        >
      Slice mid_key(key_ptr, non_shared);
      if (Compare(mid_key, target) < 0) {
        // Key at "mid" is smaller than "target".  Therefore all
        // blocks before "mid" are uninteresting.
        left = mid;
      } else {
        // Key at "mid" is >= "target".  Therefore all blocks at or
        // after "mid" are uninteresting.
        right = mid - 1;
      }
    }

    <!跳到left重启点指向的Entry,然后线性往下搜索。>
    // Linear search (within restart block) for first key >= target
    SeekToRestartPoint(left);
    while (true) {
      if (!ParseNextKey()) {
        return;
      }
      if (Compare(key_, target) >= 0) {
        return;
      }
    }
  }

  <!跳到第一个Entry,ParseNextKey会解析出第一个Key和其对应的Value>
  void SeekToFirst() override {
    SeekToRestartPoint(0);
    ParseNextKey();
  }
  
  <!跳到最后一个重启点指向的Entry,
    然后循环向下解析比较,知道最后一个Entry。
   >
  void SeekToLast() override {
    SeekToRestartPoint(num_restarts_ - 1);
    while (ParseNextKey() && NextEntryOffset() < restarts_) {
      // Keep skipping
    }
  }

 private:
  
  <!解析到错误的entry所要执行的动作。
   好像所有的流程,只有解析异常了都会
   将current_指向restart[]起始处,
   restart_index为num_restarts_
  >
  void CorruptionError() {
    current_ = restarts_;
    restart_index_ = num_restarts_;
    status_ = Status::Corruption("bad entry in block");
    key_.clear();
    value_.clear();
  }
  
  <!解析下一个Key>
  bool ParseNextKey() {
    
    <!1、求出下一个Key的offset>
    current_ = NextEntryOffset();
    <!2、计算出实际的地址>
    const char* p = data_ + current_;
    <!3、计算出重启点数组的实际地址>
    const char* limit = data_ + restarts_;  // Restarts come right after data
    <!4、如果Entry地址>=limit这地址,表示已经无entry数据了,
      返回false状态。
      >
    if (p >= limit) {
      // No more entries to return.  Mark as invalid.
      current_ = restarts_;
      restart_index_ = num_restarts_;
      return false;
    }

    // Decode next entry
    <!5、解析出下一个Entry值,并查询出其结构的各个值>
    uint32_t shared, non_shared, value_length;
    p = DecodeEntry(p, limit, &shared, &non_shared, &value_length);
    if (p == nullptr || key_.size() < shared) {
      CorruptionError();
      return false;
    } else {
      key_.resize(shared);
      key_.append(p, non_shared);
      value_ = Slice(p + non_shared, value_length);
      
      <!如果当前current_的偏移已经大于其对应重启点的下一个重启点所指向
        的偏移位,则表示current_已不在当前重启点范围内了,所以需要将
        当前current_重启点索引后移为下一个。个人感觉这里应该是为了解决
        偏移的下一个Entry已跨到下一个重启点范围了,所以需要重新更新下
        当前current_的重启点索引,但是这里用的判断条件是< current_,
        也就是说当跨到下一个重启点指向的第一个Entry,不会立马更新current_当前索引,需要跨到下一个重启点的第二个Entry才会更新这个index。
        不知道为什么不直接改成<=current_判断。
        >
      while (restart_index_ + 1 < num_restarts_ &&
             GetRestartPoint(restart_index_ + 1) < current_) {
        ++restart_index_;
      }
      return true;
    }
  }
};

<!这就是新建一个迭代器,并做一些异常检测>
Iterator* Block::NewIterator(const Comparator* comparator) {
  if (size_ < sizeof(uint32_t)) {
    return NewErrorIterator(Status::Corruption("bad block contents"));
  }
  const uint32_t num_restarts = NumRestarts();
  if (num_restarts == 0) {
    return NewEmptyIterator();
  } else {
    return new Iter(comparator, data_, restart_offset_, num_restarts);
  }
}

}  // namespace leveldb

总结

重启点间隔参数表示两个重启点之间的Key个数,原文中的说明如下:

// Number of keys between restart points for delta encoding of keys.
  // This parameter can be changed dynamically.  Most clients should
  // leave this parameter alone.
  int block_restart_interval = 16;

有两点要说明下:

  1. 压缩率: 因为用户存储的key,一般都是按一定规则来的,所以前后key肯定有相同的部分,
    所以通过设置重启点间隔,相对于对key进行压缩,减少存储的数据。
  2. 数据损坏: 但是有一点要注意,如果重启点指向的key损坏了,则接下来的key(直至下一个重启点),都是无法解析的,即数据是损坏的。所以这里也是不建议使用者去改动,除非使用者能很好的控制。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章