當前位置:首頁 > 芯聞號 > 充電吧
[導讀]一.Block的存儲格式Block的種類很多,包括Data Block、Meta Block等,每個Block由三部分組成,如下圖所示: 1.block data block data是具體的KV對

一.Block的存儲格式

Block的種類很多,包括Data Block、Meta Block等,每個Block由三部分組成,如下圖所示:


1.block data
block data是具體的KV對存儲區(qū)域。雖然Block有好幾種,但是block data都是有序的KV對,因此寫入、讀取block data的接口都是統(tǒng)一的。
2.type
type指明使用的是哪種壓縮方式,當前支持none和snappy壓縮。
3.crc32
數(shù)據(jù)校驗位
LevelDB對block data的管理是讀寫分離的,讀取后的遍歷查詢操作由Block類實現(xiàn),block data的構(gòu)建則由BlockBuilder類實現(xiàn)。
二.block data的結(jié)構(gòu)
假設(shè)每一個KV對是一條記錄(Record),則記錄的格式如下。
——共享前綴長度 ? ? ? ? ? ? ? ? ? ? shared_bytes: ? ? ? varint32
——前綴之后的字符串長度 ? ? ? ?unshared_bytes: ? varint32
——值的長度 ? ? ? ? ? ? ? ? ? ? ? ? ? value_length: ? ? ? ?varint32
——前綴之后的字符串 ? ? ? ? ? ? ?key_delta: ? ? ? ? ? ? char[unshared_bytes]
——值 ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?value: ? ? ? ? ? ? ? ? ? ?char[value_length]
對于重啟點而言,shared_bytes = 0,重啟點存儲完整的key。
block data的結(jié)尾段格式是:
——restarts: ? ? ?uint32[num_restarts]
——num_restarts: ?uint32?
尾段存儲的是重啟點相關(guān)信息,包括重啟點的位置和個數(shù)。元素restarts[i]存儲的是block data第i個重啟點距離block data首地址的偏移。很明顯第一條記錄,總是第一個重啟點,也就是restarts[0] = 0。num_restarts是重啟點的個數(shù)。

block data的結(jié)構(gòu)圖如下所示:


總體來看block data可分為KV存儲區(qū)和重啟點信息存儲區(qū)兩部分。

三.block data的構(gòu)建
block data的構(gòu)建是通過BlockBuilder實現(xiàn)的,BlockBuilder類的頭文件如下所示。

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

  // 重置BlockBuilder
  void Reset();

  // Add的調(diào)用應該在Reset之后,在Finish之前。
  // Add只添加KV對(一條記錄),重啟點信息部分由Finish添加。
  // 每次調(diào)用Add時,key應該越來越大。
  void Add(const Slice& key, const Slice& value);

  // 組建block data完成,返回block data
  Slice Finish();

  // 估算當前block data的大小
  size_t CurrentSizeEstimate() const;

  // 是否已經(jīng)開始組建block data
  bool empty() const {
    return buffer_.empty();
  }

 private:
  const Options*        options_;     
  std::string           buffer_;      // 用于存放block data
  std::vector restarts_;    // 用于存放重啟點的位置信息
  int                   counter_;     // 從上個重啟點遍歷到下個重啟點時的計數(shù)
  bool                  finished_;    // 是否調(diào)用了Finish
  std::string           last_key_;    // 記錄最后Add的key

  // No copying allowed
  BlockBuilder(const BlockBuilder&);
  void operator=(const BlockBuilder&);
};
下面重點介紹BlockBuilder類中的方法。
1.構(gòu)造函數(shù)
BlockBuilder::BlockBuilder(const Options* options)
    : options_(options),
      restarts_(),
      counter_(0),
      finished_(false) {
  assert(options->block_restart_interval >= 1);
  restarts_.push_back(0);   
}
options->block_restart_interval表示當前重啟點(其實也是一條記錄)和上個重啟點之間間隔了多少條記錄。
restarts_.push_back(0),表示第一個重啟點距離block data起始位置的偏移為0,也就是說第一條記錄就是重啟點。
2.Add函數(shù)
void BlockBuilder::Add(const Slice& key, const Slice& value) {
  Slice last_key_piece(last_key_);
  assert(!finished_);
  assert(counter_ <= options_->block_restart_interval);
  assert(buffer_.empty() // No values yet?
         || options_->comparator->Compare(key, last_key_piece) > 0);
  size_t shared = 0;
  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 {
    // 如果counter_=options_->block_restart_interval,說明這條記錄就是重啟點。
	// 將這條記錄距離block data首地址的偏移添加到restarts_中,并使counter_ = 0,
    restarts_.push_back(buffer_.size());
    counter_ = 0;
  }
  const size_t non_shared = key.size() - shared;

  // 開始組建一條記錄
  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());

  // 此時last_key_還等于上一個key。resize用于取出last_key_與當前key中相同的前綴
  last_key_.resize(shared);
  // last_key_添加當前key中與上一個key的不同部分,此時last_key_與當前key是相等的。
  last_key_.append(key.data() + shared, non_shared);
  // 上面兩句其實等效于last_key_=key.ToString(),但是像上面那樣寫可以使內(nèi)存copy最小化
  assert(Slice(last_key_) == key);
  counter_++;
}
在求相同前綴的長度時,為何要調(diào)用std::min來計算上一個key(last_key_piece)和當前key的長度呢?因為當前key雖然比上一個key大(通過Compare得出),但是不一定就比上一個key長。比如mouse和morning,由于u大于r,mouse是大于moring的。關(guān)于comparator可以參考:LevelDB源碼分析之二:comparator
需要注意的是,為了節(jié)約存儲空間,每條記錄的前三個字段是被壓縮存儲的(通過PutVarint32實現(xiàn)),關(guān)于壓縮,詳見:LevelDB源碼分析之一:coding
3.Finish函數(shù)
Slice BlockBuilder::Finish() {
  // 添加重啟點信息部分
  for (size_t i = 0; i < restarts_.size(); i++) {
    PutFixed32(&buffer_, restarts_[i]);
  }
  PutFixed32(&buffer_, restarts_.size());
  finished_ = true;
  return Slice(buffer_);
}
Finish只是在記錄存儲區(qū)后邊添加了重啟點信息,重啟點信息沒有進行壓縮,關(guān)于PutFixed32也可以參考:LevelDB源碼分析之一:coding
假設(shè)添加的5個KV對分別是("the bus","1"),("the car","11"),("the color","111"),("the mouse","1111"),("the tree","11111"),那么當options_->block_restart_interval=3時,block data的示意圖如下所示。


四.block data的解析

block data的解析是通過Block類實現(xiàn)的。

inline uint32_t Block::NumRestarts() const {
  // size_為何要大于等于2*sizeof(uint32_t),因為如果只調(diào)用BlockBuilder中
  // 的Finish函數(shù),那么block data至少包含一個uint32_t類型的重啟點位置信息和
  // 一個uint32_t類型的重啟點個數(shù)信息
  assert(size_ >= 2*sizeof(uint32_t));
  // block data的最后一個uint32_t類型字段表示重啟點個數(shù)。
  return DecodeFixed32(data_ + size_ - sizeof(uint32_t));
}

Block::Block(const char* data, size_t size)
    : data_(data),
      size_(size) {
  if (size_ < sizeof(uint32_t)) {
    size_ = 0; // 出錯時,使size_=0
  } else {
	// 總大小減去重啟點信息的大小,重啟點信息包括重啟點位置數(shù)組和重啟點個數(shù),
	// 他們都是uint32_t類型的
    restart_offset_ = size_ - (1 + NumRestarts()) * sizeof(uint32_t);
	// 這里的條件判斷是防止NumRestarts()返回值為負數(shù)
    if (restart_offset_ > size_ - sizeof(uint32_t)) {
      size_ = 0;
    }
  }
}

Block::~Block() {
  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 derefence past "limit".
//

// 解析從block data的p位置開始的數(shù)據(jù),將解析得到的shared、non_shared和value length
// 分別放到"*shared"、"*non_shared"和"*value_length"
// 從源碼看出,p應該是一條記錄的起始位置
// 如果解析錯誤,返回NULL。否則,返回指向一條記錄的key_delta字段的指針
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 NULL;
  *shared = reinterpret_cast(p)[0];
  *non_shared = reinterpret_cast(p)[1];
  *value_length = reinterpret_cast(p)[2];
  // 如果最高位都是0, 那么按照壓縮規(guī)則,每個值只占一個字節(jié),且小于128
  // 這里相當于做了一個優(yōu)化,如果三個值之和都小于128,那肯定是每個值只占一個字節(jié) 
  if ((*shared | *non_shared | *value_length) < 128) {
    p += 3;
  } else {
    if ((p = GetVarint32Ptr(p, limit, shared)) == NULL) return NULL;
    if ((p = GetVarint32Ptr(p, limit, non_shared)) == NULL) return NULL;
    if ((p = GetVarint32Ptr(p, limit, value_length)) == NULL) return NULL;
  }
  // 如果limit中剩下的空間小于*non_shared、*value_length之和,說明limit中
  // 已經(jīng)容納不下記錄中的key_delta和value字段了。
  if (static_cast(limit - p) < (*non_shared + *value_length)) {
    return NULL;
  }
  return p;
}

class Block::Iter : public Iterator {
 private:
  const Comparator* const comparator_;
  const char* const data_;      // block data
  uint32_t const restarts_;     // 重啟點信息在block data中的偏移  
  uint32_t const num_restarts_; // 重啟點個數(shù)

  // current_是當前記錄在bock data中的偏移,如果current_>=restarts_,說明出錯啦。
  uint32_t current_;
  uint32_t restart_index_; // 重啟點的索引
  std::string key_;
  Slice value_;
  Status status_;

  inline int Compare(const Slice& a, const Slice& b) const {
    return comparator_->Compare(a, b);
  }

  // 因為value_是一條記錄的最后一個字段,所以這里返回的是下一條記錄的偏移量,也就是current_
  // 但是如果在該函數(shù)之前調(diào)用了SeekToRestartPoint,此時的value_.data()=data_,value.size=0
  // 這樣的話即使是block data的第一條記錄,也可以用使用該函數(shù),此時返回的偏移量為0
  inline uint32_t NextEntryOffset() const {
    return (value_.data() + value_.size()) - data_;
  }
  // 獲取第index個重啟點的偏移
  uint32_t GetRestartPoint(uint32_t index) {
    assert(index < num_restarts_);
    return DecodeFixed32(data_ + restarts_ + index * sizeof(uint32_t));
  }
  // 該函數(shù)只是設(shè)置了幾個有限的狀態(tài),其它值將在函數(shù)ParseNextKey()中設(shè)置。
  // 需要注意的是,這里的value_并不是記錄中的value字段,而只是一個指向記錄起始位置的0長度指針,
  // 這樣后面的ParseNextKey函數(shù)將會解析出重啟點的value字段,并賦值到value_中。
  void SeekToRestartPoint(uint32_t index) {
    key_.clear();
    restart_index_ = index;
    // current_ will be fixed by ParseNextKey();
	// current_的值會在ParseNextKey()方法中不斷被修改
    // ParseNextKey() starts at the end of value_, so set value_ accordingly
    uint32_t offset = GetRestartPoint(index);
    value_ = Slice(data_ + offset, 0);
  }

 public:
  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);
  }

  virtual bool Valid() const { return current_ < restarts_; }
  virtual Status status() const { return status_; }
  virtual Slice key() const {
    assert(Valid());
    return key_;
  }
  virtual Slice value() const {
    assert(Valid());
    return value_;
  }
  // 向后解析比較簡單,就是解析當前記錄的下一個記錄
  virtual void Next() {
    assert(Valid());
    ParseNextKey();
  }
  // 向前解析復雜一些,步驟如下
  // 1.先向前查找當前記錄之前的重啟點
  // 2.當循環(huán)到了第一個重啟點,其偏移量(0)依然與當前記錄的偏移量相等
  //   說明當前記錄就是第一條記錄,此時初始化current_和restart_index_,并返回
  // 3.調(diào)用SeekToRestartPoint定位到符合要求的啟動點
  // 4.向后循環(huán)解析,直到解析了原記錄之前的一條記錄,結(jié)束
  virtual void Prev() {
    assert(Valid());

    const uint32_t original = current_;
    while (GetRestartPoint(restart_index_) >= original) {
      if (restart_index_ == 0) {
        // No more entries
        current_ = restarts_;
        restart_index_ = num_restarts_;
        return;
      }
      restart_index_--;
    }
	
    SeekToRestartPoint(restart_index_);
    do {
      // Loop until end of current entry hits the start of original entry
    } while (ParseNextKey() && NextEntryOffset() < original);
  }
  // 從左到右(從前到后)查找第一條key大于target的記錄 
  // 1.二分查找,找到key < target的最后一個重啟點
  // 2.定位到該重啟點,其索引由left指定,這是前面二分查找到的結(jié)果。如前面所分析的,
  //   value_指向重啟點的地址,而size_指定為0,這樣ParseNextKey函數(shù)將會解析出重啟點key和value。
  // 3.自重啟點線性向下查找,直到遇到key>=target的記錄或者直到最后一條記錄,也不滿足key>=target,返回
  virtual void Seek(const Slice& target) {
    // Binary search in restart array to find the first restart point
    // with a key >= target
    uint32_t left = 0;
    uint32_t right = num_restarts_ - 1;
    while (left < right) {
      uint32_t mid = (left + right + 1) / 2;
      uint32_t region_offset = GetRestartPoint(mid);
      uint32_t shared, non_shared, value_length;
      const char* key_ptr = DecodeEntry(data_ + region_offset,
                                        data_ + restarts_,
                                        &shared, &non_shared, &value_length);
	  // 需要注意的是重啟點保存的key是完整的,所以它的shared字段等于0
      if (key_ptr == NULL || (shared != 0)) {
        CorruptionError();
        return;
      }
      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;
      }
    }

    // Linear search (within restart block) for first key >= target
    SeekToRestartPoint(left);
    while (true) {
      if (!ParseNextKey()) {
        return;
      }
      if (Compare(key_, target) >= 0) {
        return;
      }
    }
  }
  // 解析block data的第一條記錄
  virtual void SeekToFirst() {
	//先定位到第一個重啟點
    SeekToRestartPoint(0);
    ParseNextKey();
  }
  // 解析block data的最后一條記錄
  virtual void SeekToLast() {
    // 先定位到最后一個重啟點
    SeekToRestartPoint(num_restarts_ - 1);
    while (ParseNextKey() && NextEntryOffset() < restarts_) {
      // Keep skipping
    }
  }

 private:
  void CorruptionError() {
    current_ = restarts_;
    restart_index_ = num_restarts_;
    status_ = Status::Corruption("bad entry in block");
    key_.clear();
    value_.clear();
  }

  bool ParseNextKey() {
    current_ = NextEntryOffset();
    const char* p = data_ + current_;// 指向當前記錄
    const char* limit = data_ + restarts_;  // limit限制了記錄存儲區(qū)的范圍
    if (p >= limit) {
      // 如果出錯,恢復到默認值,并返回false
      current_ = restarts_;
      restart_index_ = num_restarts_;
      return false;
    }

    // Decode next entry
    uint32_t shared, non_shared, value_length;
    p = DecodeEntry(p, limit, &shared, &non_shared, &value_length);
    if (p == NULL || key_.size() < shared) {
      CorruptionError();
      return false;
    } else {
	  // 解析出記錄中的key和value
      key_.resize(shared);
      key_.append(p, non_shared);
      value_ = Slice(p + non_shared, value_length);
	  // 如果你當前記錄的偏移已經(jīng)比下一個重啟點的偏移還有大了
	  // 那么關(guān)鍵點索引restart_index_加1,且后面記錄的解析都是
	  // 以這個重啟點為參照的。
	  // 因為restart_index_=0的重啟點就是block data的第一條記錄
	  // 所以下一個重啟點的索引是restart_index_ + 1
      while (restart_index_ + 1 < num_restarts_ &&
             GetRestartPoint(restart_index_ + 1) < current_) {
        ++restart_index_;
      }
      return true;
    }
  }
};

Iterator* Block::NewIterator(const Comparator* cmp) {
  if (size_ < 2*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(cmp, data_, restart_offset_, num_restarts);
  }
}

初始化迭代器時,為什么是把current設(shè)置為restarts,把restart_index_設(shè)置為num_restarts_?
創(chuàng)建一個Block::Itr之后,它是處于invalid狀態(tài)的,即不能Prev也不能Next;只能先Seek/SeekToxxx之后,才能調(diào)用next/prev。想想和std的iterator行為很像吧,比如你聲明一個vector::iterator,必須先賦值才能使用。

一個問題,既然通過Comparator可以極大的節(jié)省key的存儲空間,那為什么又要使用重啟點機制來額外占用一下空間呢?這是因為如果最開頭的記錄數(shù)據(jù)損壞,其后的所有記錄都將無法恢復。為了降低這個風險,leveldb引入了重啟點,每隔固定條數(shù)記錄會強制加入一個重啟點,這個位置的Entry會完整的記錄自己的Key。


參考鏈接:http://blog.csdn.net/sparkliang/article/details/8635821


本站聲明: 本文章由作者或相關(guān)機構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內(nèi)容真實性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫毥谦F公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

加利福尼亞州圣克拉拉縣2024年8月30日 /美通社/ -- 數(shù)字化轉(zhuǎn)型技術(shù)解決方案公司Trianz今天宣布,該公司與Amazon Web Services (AWS)簽訂了...

關(guān)鍵字: AWS AN BSP 數(shù)字化

倫敦2024年8月29日 /美通社/ -- 英國汽車技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時1.5...

關(guān)鍵字: 汽車 人工智能 智能驅(qū)動 BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務能7×24不間斷運行,同時企業(yè)卻面臨越來越多業(yè)務中斷的風險,如企業(yè)系統(tǒng)復雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報道,騰訊和網(wǎng)易近期正在縮減他們對日本游戲市場的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會開幕式在貴陽舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導體

8月28日消息,在2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會上,華為常務董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機 衛(wèi)星通信

要點: 有效應對環(huán)境變化,經(jīng)營業(yè)績穩(wěn)中有升 落實提質(zhì)增效舉措,毛利潤率延續(xù)升勢 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務引領(lǐng)增長 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競爭力 堅持高質(zhì)量發(fā)展策略,塑強核心競爭優(yōu)勢...

關(guān)鍵字: 通信 BSP 電信運營商 數(shù)字經(jīng)濟

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺與中國電影電視技術(shù)學會聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會上宣布正式成立。 活動現(xiàn)場 NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會上,軟通動力信息技術(shù)(集團)股份有限公司(以下簡稱"軟通動力")與長三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉
關(guān)閉