版权声明:本文为博主原创文章,未经博主允许不得转载。
leveldb
作为一个 key-value
数据库,它和 redis
的区别在于不仅没有把所有的数据放在内存中,而是把大部分数据放在了磁盘中
leveldb
存数据的流程
- 先指定一块内存写数据(这块内存称为
MemTable
),当占用的内存高于阈值后,将这块内存转为只读(这块只读内存称为 Immutable MemTable
)
- 同时开辟一块新的内存(
MemTable
)来写数据
- 然后异步将
Immutable MemTable
的数据添加到存到磁盘中
本文将从数据怎么写入磁盘开始讲起(第 3
步中的数据存储到磁盘),数据写入磁盘时会先将数据放在一个名叫 table
的结构中,table
中包含有很多个 block
,block
是真正用来存储 key-value
的
当 table
的大小大于指定阈值时,就会把 table
中的数据写入到磁盘中
table
结构
1 2 3 4 5 6 7 8 9 10 11 12
| <beginning_of_file> [data block 1] [data block 2] ... [data block N] [meta block 1] ... [meta block K] [metaindex block] [index block] [Footer] (fixed size; starts at file_size - sizeof(Footer)) <end_of_file>
|
第一眼看着不明觉厉,这是个什么鬼玩意呢
其实 table
就是用来存放多个 data block
的,那问题来了,下面一堆 meta block
/ metaindex block
/ index block
是什么鬼?
当我们在 table
里面查询一个 key
,在没有索引的情况下,就需要将所有的 data block
遍历一遍,如果按照这种方式找的话,十亿级的数据存储找到猴年马月才能找到,怎样才能高效的找到我们需要查询的 key
呢,就靠上面所说的那堆鬼东西,这里我们按下不表,后面会详细介绍(挖坑1)
上面说了这么多,只是为了让大家有个基本的概念,本章重点要讲解的是 table
中 data block
data block
万丈高楼平地起,我们现在来介绍 Leveldb
中最基础的结构,也是真正存储 KV
的结构 data block
搞懂 data block
需要阅读如下源码文件
1 2 3 4 5 6 7
| 1 table/block.h 2 table/block.cc 3 table/block_build.h 4 table/block_build.cc 5 include/leveldb/slice.h 6 util/coding.h 7 util/coding.cc
|
slice.h (简单了解后可跳过)
先介绍最简单的 slice
,实际就是把 string
封装了一下,它拥有两个私有变量和几个简单的处理函数
1 2 3 4 5 6 7 8 9 10
| public: char operator[](size_t n) const; void remove_prefix(size_t n); std::string ToString(); int compare(const Slice& b); bool starts_with(const Slice& x); private: const char* data_; size_t size_;
|
涉及的小知识 (专为像我这样的新手准备,请看 【Leveldb源码解析新手补充篇】)
- 符号重载
size_t
类型
inline
内联函数
- 函数后面
const
有啥作用
coding.h 和 coding.cc
coding
这个类是用来类型转换和压缩空间的,比如声明一个 uint32
,我们要分配4个字节的空间,如果这个 uint32
只是存储数字 1
,那就太浪费了,一个字节就能搞定,浪费 3
个字节,而 Leveldb
中记录长度的 uint
变量非常多,不压缩的话就会浪费大量的空间
这里只贴在 data block
部分中用到的两个函数,更多关于 coding
类的源码分析请看 【Leveldb源码解析工具篇之coding类】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| inline uint32_t DecodeFixed32(const char* ptr) { 高/低字节:从高位到低位的字节依次是0x12、0x34、0x56和0x78 高/低地址:大端模式 高位高地址,低位低地址;小端模式 低位高地址,高位低地址 大端模式:高地址 0x78|0x56|0x34|0x12 低地址 小端模式:高地址 0x12|0x34|0x56|0x78 低地址 */ if (port::kLittleEndian) { uint32_t result; memcpy(&result, ptr, sizeof(result)); return result; } else { return ((static_cast<uint32_t>(static_cast<unsigned char>(ptr[0]))) | (static_cast<uint32_t>(static_cast<unsigned char>(ptr[1])) << 8) | (static_cast<uint32_t>(static_cast<unsigned char>(ptr[2])) << 16) | (static_cast<uint32_t>(static_cast<unsigned char>(ptr[3])) << 24)); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| void PutVarint32(std::string* dst, uint32_t v) { char buf[5]; char* ptr = EncodeVarint32(buf, v); dst->append(buf, ptr - buf); } 编码的目的是为将int存储小数字时所浪费的空间节省下来,有可能节省1个字节,有可能节省2个字节,也有可能节省3个字节 节省的空间不同,int编码后占用的空间也就不固定了 如果直接将int放在string中,那么在string中解析这个int时只需读取4个字节,然后再转成int就是我们所要的int值 我们为什么知道这地方要读取4个字节,因为一个int占4个字节,读取4个字节这个int就结束了 但是编码后的int大小不固定,在string中解析编码后的int,怎么知道要读几个字节 一个字节是8位,大神将一个字节分为两个部分,最高位和低7位,低7位用来存数据,最高位用来表示有没有结束,举个栗子 值为300的int变量,二进制为0x100101100,在内存中实际只用两个字节,浪费两个字节 在内存中是这么存的 |0|0|0|0|0|0|0|1| 0|0|1|0|1|1|0|0| 而编码后在内存中是这么存的 |0|0|0|0|0|0|1|0| 1|0|1|0|1|1|0|0| 区别在哪儿呢,就是在第8位中插入一位,设置为1,为什么要怎么做 我们上面有讲,一个字节中7位用来存数据,1位表示是否结束,当我们读到一个字节时,判断最高位是否为1 如果是1,说明编码后的int还没有结束,需要继续读下一个字节,直到读到字节的最高位为0为止 PutVarint32 中定义的char数组长度为啥是5呢,在编码过程中,每个字节会拿出1bit来存储于是否结束标识 如果有一个int变量的值本身就占据4个字节,再加上每个字节最高位来存储结束标识,4个字节肯定是不够的,所以定义长度为5的字符数组来存储编码后的int */ char* EncodeVarint32(char* dst, uint32_t v) { unsigned char* ptr = reinterpret_cast<unsigned char*>(dst); static const int B = 128; if (v < (1<<7)) { *(ptr++) = v; } else if (v < (1<<14)) { *(ptr++) = v | B; *(ptr++) = v>>7; } else if (v < (1<<21)) { *(ptr++) = v | B; *(ptr++) = (v>>7) | B; *(ptr++) = v>>14; } else if (v < (1<<28)) { *(ptr++) = v | B; *(ptr++) = (v>>7) | B; *(ptr++) = (v>>14) | B; *(ptr++) = v>>21; } else { *(ptr++) = v | B; *(ptr++) = (v>>7) | B; *(ptr++) = (v>>14) | B; *(ptr++) = (v>>21) | B; *(ptr++) = v>>28; } return reinterpret_cast<char*>(ptr); }
|
涉及的小知识 (专为像我这样的新手准备,请看 【Leveldb源码解析新手补充篇】)
block
上面讲解了一下在 block
中用的工具类,现在开始进入主题,介绍一下 block
的结构(table
和 block
的结构一定要牢牢记住了)
1 2 3 4 5 6 7 8 9 10 11 12
| [K-V Record 0] <------------------ [K-V Record 1] | ... | [K-V Record 16] <-------- | [K-V Record 17] | | ... | | [K-V Record n-1] | | [restart point 0]-------|--------| [restart point 1]-------| ... [restart point (n+15)/16] [restart point number]
|
K-V Record
里面存储的是 key value
值,在 block
中存储的 key-value
是顺序存储的,所以会出现多个连续的 key
有部分前缀是相同的,leveldb
为了压缩存储,会压缩前缀(具体请往后看),一行记录在 block
具体存储格式为
1
| [key中相同部分大小][key中不同部分大小][value大小][key中不同部分值][value值]
|
restart point
在前缀压缩时,只记录了 key
中不同部分的数据,相同部分的数据需要从上一个 key
中获取,如果要获取第100个 key
,那么就需要把前 99
个 key
全部遍历出来,这样做的话会使查找 key
时效率很低,leveldb
的做法是相邻的 16
个 key
做一次前缀压缩,restart point
就是记录每 16
个 key
中第一个 key
的偏移量,举个栗子
1 2 3 4 5 6
| 0 [key中相同部分大小][key中不同部分大小][value大小][key中不同部分值][value值] 1 [key中相同部分大小][key中不同部分大小][value大小][key中不同部分值][value值] 2 [key中相同部分大小][key中不同部分大小][value大小][key中不同部分值][value值] ... 16 [key中相同部分大小][key中不同部分大小][value大小][key中不同部分值][value值] 17 [key中相同部分大小][key中不同部分大小][value大小][key中不同部分值][value值]
|
这里有 18
行记录,假如存储数据如下:(中间记录忽略)
1 2 3 4 5 6
| 第0行 [helloantony : antonyvalue] 第1行 [helloatom : atomvalue ] 第2行 [helloboy : boyvalue ] ... 第16行 [worldantony : antonyvalue] 第17行 [worldatom : atomvalue ]
|
在 data block
中真正存储的情况如下
1 2 3 4 5 6
| [0][11][11][helloantony][antonyvalue] [6][ 3][ 9][tom][atomvalue] [5][ 3][ 8][boy][boyvalue] ... [0][11][11][worldantony][antonyvalue] [6][ 3][ 9][tom][atomvalue]
|
我们会看到第 0
行和第 16
行的 key
是完整保存的,这两行也是重启点所指向的点,为了方便理解,上面的结构中一条记录用一行显示,但实际存储中这些记录都是存储在一个长字符串中,重启点记录的是第 0
行和第 16
行在字符串中的偏移量,通过这些偏移就能能快速定位到这两个点,可以理解为索引
重启点记录的偏移量怎么计算会在代码中详细讲解(挖坑2)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class Block { public: explicit Block(const BlockContents& contents); ~Block(); size_t size() const { return size_; } Iterator* NewIterator(const Comparator* comparator); private: uint32_t NumRestarts() const; const char* data_; size_t size_; uint32_t restart_offset_; bool owned_; Block(const Block&); void operator=(const Block&); class Iter; };
|
构造函数中出现了一个结构体 BlockContents
,这个结构体是在 table/format.h
中定义的
1 2 3 4 5
| struct BlockContents { Slice data; bool cachable; bool heap_allocated; };
|
block
构造方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| Block::Block(const BlockContents& contents) : data_(contents.data.data()), size_(contents.data.size()), owned_(contents.heap_allocated) { if (size_ < sizeof(uint32_t)) { size_ = 0; } else { size_t max_restarts_allowed = (size_-sizeof(uint32_t)) / sizeof(uint32_t); if (NumRestarts() > max_restarts_allowed) { size_ = 0; } else { restart_offset_ = size_ - (1 + NumRestarts()) * sizeof(uint32_t); } } }
|
block
构造方法说完,这里仅仅是 block
的结构,真正来来操作这个结构的类是 blockbuilder
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| class BlockBuilder { public: explicit BlockBuilder(const Options* options); void Reset(); void Add(const Slice& key, const Slice& value); Slice Finish(); size_t CurrentSizeEstimate() const; bool empty() const { return buffer_.empty(); } private: const Options* options_; std::string buffer_; std::vector<uint32_t> restarts_; int counter_; bool finished_; std::string last_key_; BlockBuilder(const BlockBuilder&); void operator=(const BlockBuilder&); }; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| BlockBuilder::BlockBuilder(const Options* options) : options_(options), restarts_(), counter_(0), finished_(false) { assert(options->block_restart_interval >= 1); restarts_.push_back(0); } void BlockBuilder::Reset() { buffer_.clear(); restarts_.clear(); restarts_.push_back(0); counter_ = 0; finished_ = false; last_key_.clear(); } size_t BlockBuilder::CurrentSizeEstimate() const { return (buffer_.size() + restarts_.size() * sizeof(uint32_t) + sizeof(uint32_t)); } 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() || options_->comparator->Compare(key, last_key_piece) > 0); size_t shared = 0; if (counter_ < options_->block_restart_interval) { 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 { 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()); buffer_.append(key.data() + shared, non_shared); buffer_.append(value.data(), value.size()); 这个地方有点意思,为啥不直接将key赋值给last_key_,这让我纳闷的很久,隐约可以看出是为了考虑效率? 我把自己的测试结果讲解一下,如有不对还请大家指出 last_key_是string类型,string中有两个成员变量,size和capacity,size表示当前string的长度是多少,capacity是string真实容量 如果我们直接赋值的话string会先将之前存储的数据释放,然后调用隐式构造为last_key_重新分配空间,效率低 如果我们调用resize的话,last_key_仅仅是将shared后面的字节截掉,capacity不会发生改变,也不会重新分配空间 再将non_shared部分添加到last_key_中,最后的效果一样,但是效率却千差万别 */ last_key_.resize(shared); last_key_.append(key.data() + shared, non_shared); assert(Slice(last_key_) == key); counter_++; } 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_); }
|
【作者:antonyxu https://antonyxux.github.io/ 】