DataBlock屬於SSTable中Block的一種,關於DataBlock與SSTable的關係,可點此鏈接查看SSTable結構說明。
本篇主要是對DataBlock的讀寫流程解讀。這裏爲便於理解,則將DataBlock格式再次放出:
寫流程
寫流程很簡單,就是按照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;
有兩點要說明下:
- 壓縮率: 因爲用戶存儲的key,一般都是按一定規則來的,所以前後key肯定有相同的部分,
所以通過設置重啓點間隔,相對於對key進行壓縮,減少存儲的數據。 - 數據損壞: 但是有一點要注意,如果重啓點指向的key損壞了,則接下來的key(直至下一個重啓點),都是無法解析的,即數據是損壞的。所以這裏也是不建議使用者去改動,除非使用者能很好的控制。