【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(直至下一個重啓點),都是無法解析的,即數據是損壞的。所以這裏也是不建議使用者去改動,除非使用者能很好的控制。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章