leveldb

记录一下我看levelDB的流程:
环境的搭建:Ubuntu+eclipse+cpp插件
http://zouzls.github.io/2016/11/26/LevelDB%E4%B9%8B%E6%BA%90%E7%A0%81%E8%B0%83%E8%AF%95%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA/
注意几点:leveldb源码如果按照这里下载不了,可以自己去GitHub中去下

levelDB原理分析:http://blog.sina.com.cn/s/blog_999d1f4c01010e51.html
http://blog.sina.com.cn/s/blog_999d1f4c01010mj9.html

http://blog.csdn.net/sparkliang/article/details/8604416

一 memtable
memtable:http://blog.csdn.net/xuqianghit/article/details/6948164

InternalKey的格式为:

| User key (string) | sequence number (7 bytes) | value type (1 byte) |

由此还可知道sequence number大小是7 bytes,sequence number是所有基于op log系统的关键数据,它唯一指定了不同操作的时间顺序。

跳表:http://www.cnblogs.com/xuqiang/archive/2011/05/22/2053516.html
http://kenby.iteye.com/blog/1187303

Arena内存管理:http://mingxinglai.com/cn/2013/01/leveldb-arena/

变长编码:http://brg-liuwei.github.io/tech/2014/10/20/leveldb-4.html
//每七位增加一个补充位(变长编码,用一位来表示是否结束,其他的7位表数值)
int VarintLength(uint64_t v) {
int len = 1;
while (v >= 128) {
v >>= 7;
len++;
}
return len;
}

二 cache
http://blog.csdn.net/sparkliang/article/details/8567602
http://brg-liuwei.github.io/tech/2014/11/17/leveldb-7.html
http://blog.csdn.net/u012658346/article/details/45486051

这里写图片描述
1.LRUHandle结构体
void* value; 这个存储的是cache的数据;

void (deleter)(const Slice&, void value);这个是数据从Cache中清除时执行的清理函数;

后面的三个成员事关LRUCache的数据的组织结构:

LRUHandle *next_hash;

指向节点在hash table链表中的下一个hash(key)相同的元素,在有碰撞时Leveldb采用的是链表法(拉链法)。最后一个节点的next_hash为NULL。

LRUHandle *next, *prev;

节点在双向链表中的前驱后继节点指针,所有的cache数据都是存储在一个双向list中,最前面的是最新加入的,每次新加入的位置都是head->next。所以每次剔除的规则就是剔除list tail。

2.HandleTable:leveldb自己实现了一个hash table
这个类就是基本的hash操作:Lookup、Insert和Delete。Hash table的作用是根据key快速查找元素是否在cache中,并返回LRUHandle节点指针,由此就能快速定位节点在hash表和双向链表中的位置。

它是通过LRUHandle的成员next_hash组织起来的。

HandleTable使用LRUHandle **list_存储所有的hash节点,其实就是一个二维数组,一维是不同的hash(key),另一维则是相同hash(key)的碰撞list。

每次当hash节点数超过当前一维数组的长度后,都会做Resize操作:

LRUHandle** new_list = new LRUHandle*[new_length];

然后复制list_到new_list中,并删除旧的list_。

hashtable的具体实现:
class HandleTable {
public:
HandleTable() : length_(0), elems_(0), list_(NULL) { Resize(); }
~HandleTable() { delete[] list_; }

LRUHandle* Lookup(const Slice& key, uint32_t hash) {
return *FindPointer(key, hash);
}

LRUHandle* Insert(LRUHandle* h) {
LRUHandle** ptr = FindPointer(h->key(), h->hash);
LRUHandle* old = *ptr;
h->next_hash = (old == NULL ? NULL : old->next_hash);
*ptr = h;
if (old == NULL) {
++elems_;
if (elems_ > length_) {
// Since each cache entry is fairly large, we aim for a small
// average linked list length (<= 1).
Resize();
}
}
return old;
}

LRUHandle* Remove(const Slice& key, uint32_t hash) {
LRUHandle** ptr = FindPointer(key, hash);
LRUHandle* result = *ptr;
if (result != NULL) {
*ptr = result->next_hash;
–elems_;
}
return result;
}

private:
// The table consists of an array of buckets where each bucket is
// a linked list of cache entries that hash into the bucket.
uint32_t length_;
uint32_t elems_;
LRUHandle** list_;

// Return a pointer to slot that points to a cache entry that
// matches key/hash. If there is no such cache entry, return a
// pointer to the trailing slot in the corresponding linked list.
LRUHandle** FindPointer(const Slice& key, uint32_t hash) {
LRUHandle** ptr = &list_[hash & (length_ - 1)];
while (*ptr != NULL &&
((*ptr)->hash != hash || key != (*ptr)->key())) {
ptr = &(*ptr)->next_hash;
}
return ptr;
}

void Resize() {
uint32_t new_length = 4;
while (new_length < elems_) {
new_length *= 2;
}
LRUHandle** new_list = new LRUHandle*[new_length];
memset(new_list, 0, sizeof(new_list[0]) * new_length);
uint32_t count = 0;
for (uint32_t i = 0; i < length_; i++) {
LRUHandle* h = list_[i];
while (h != NULL) {
LRUHandle* next = h->next_hash;
Slice key = h->key();
uint32_t hash = h->hash;
LRUHandle** ptr = &new_list[hash & (new_length - 1)];
h->next_hash = *ptr;
*ptr = h;
h = next;
count++;
}
}
assert(elems_ == count);
delete[] list_;
list_ = new_list;
length_ = new_length;
}
};

3.基于HandleTable和LRUHandle,实现了一个标准的LRUcache,并内置了mutex保护锁,是线程安全的。

其中存储所有数据的双向链表是LRUHandle lru_,这是一个list head;

Hash表则是HandleTable table_;

4.ShardedLRUCache类,实际上到S3,一个标准的LRU Cache已经实现了,为何还要更近一步呢?答案就是速度!

为了多线程访问,尽可能快速,减少锁开销,ShardedLRUCache内部有16个LRUCache,查找Key时首先计算key属于哪一个分片,分片的计算方法是取32位hash值的高4位,然后在相应的LRUCache中进行查找,这样就大大减少了多线程的访问锁的开销。

LRUCache shard_[kNumShards]

它就是一个包装类,实现都在LRUCache类中。

三 日志
http://blog.csdn.net/sparkliang/article/details/8623779

1.日志格式log record
Leveldb把日志文件切分成了大小为32KB的连续block块,block由连续的log record组成,log record的格式为:

,注意:CRC32, Length都是little-endian的。

Log Type有4种:FULL = 1、FIRST = 2、MIDDLE = 3、LAST = 4。FULL类型表明该log record包含了完整的user record;而user record可能内容很多,超过了block的可用大小,就需要分成几条log record,第一条类型为FIRST,中间的为MIDDLE,最后一条为LAST。也就是:

FULL,说明该log record包含一个完整的user record;

FIRST,说明是user record的第一条log record

MIDDLE,说明是user record中间的log record

LAST,说明是user record最后的一条log record

2.写(即将数据从内存写入到磁盘中的日志文件中)
class Writer {
public:
// Create a writer that will append data to “*dest”.
// “*dest” must be initially empty.
// “*dest” must remain live while this Writer is in use.
explicit Writer(WritableFile* dest);

// Create a writer that will append data to “*dest”.
// “*dest” must have initial length “dest_length”.
// “*dest” must remain live while this Writer is in use.
Writer(WritableFile* dest, uint64_t dest_length);

~Writer();

Status AddRecord(const Slice& slice);

private:
WritableFile* dest_;
int block_offset_; // Current offset in block

// crc32c values for all supported record types. These are
// pre-computed to reduce the overhead of computing the crc of the
// record type stored in the header.
uint32_t type_crc_[kMaxRecordType + 1];

Status EmitPhysicalRecord(RecordType type, const char* ptr, size_t length);

// No copying allowed
Writer(const Writer&);
void operator=(const Writer&);
};
Writer::Writer(WritableFile* dest)
: dest_(dest),
block_offset_(0) {
InitTypeCrc(type_crc_);
}

Writer::Writer(WritableFile* dest, uint64_t dest_length)
: dest_(dest), block_offset_(dest_length % kBlockSize) {
InitTypeCrc(type_crc_);
}

Writer::~Writer() {
}
//唯一的接口,将数据写入到log文件中(按照log record格式)
Status Writer::AddRecord(const Slice& slice) {
const char* ptr = slice.data();
size_t left = slice.size();

// Fragment the record if necessary and emit it. Note that if slice
// is empty, we still want to iterate once to emit a single
// zero-length record
Status s;
bool begin = true;
do {
const int leftover = kBlockSize - block_offset_;
assert(leftover >= 0);
//如果这个block剩余的小于log record的头部(即7个字节),用0填补
if (leftover < kHeaderSize) {
// Switch to a new block
if (leftover > 0) {
// Fill the trailer (literal below relies on kHeaderSize being 7)
assert(kHeaderSize == 7);
dest_->Append(Slice(“\x00\x00\x00\x00\x00\x00”, leftover));
}
block_offset_ = 0;
}

// Invariant: we never leave < kHeaderSize bytes in a block.
assert(kBlockSize - block_offset_ - kHeaderSize >= 0);

const size_t avail = kBlockSize - block_offset_ - kHeaderSize;
const size_t fragment_length = (left < avail) ? left : avail;

RecordType type;
const bool end = (left == fragment_length);
if (begin && end) {
  type = kFullType;
} else if (begin) {
  type = kFirstType;
} else if (end) {
  type = kLastType;
} else {
  type = kMiddleType;
}

s = EmitPhysicalRecord(type, ptr, fragment_length);
ptr += fragment_length;
left -= fragment_length;
begin = false;

} while (s.ok() && left > 0);
return s;
}

Status Writer::EmitPhysicalRecord(RecordType t, const char* ptr, size_t n) {
assert(n <= 0xffff); // Must fit in two bytes
assert(block_offset_ + kHeaderSize + n <= kBlockSize);

// Format the header
char buf[kHeaderSize];
buf[4] = static_cast(n & 0xff);
buf[5] = static_cast(n >> 8);
buf[6] = static_cast(t);

// Compute the crc of the record type and the payload.
uint32_t crc = crc32c::Extend(type_crc_[t], ptr, n);
crc = crc32c::Mask(crc); // Adjust for storage
EncodeFixed32(buf, crc);

// Write the header and the payload
Status s = dest_->Append(Slice(buf, kHeaderSize));
if (s.ok()) {
s = dest_->Append(Slice(ptr, n));
if (s.ok()) {
s = dest_->Flush();
}
}
block_offset_ += kHeaderSize + n;//着一个block已使用的量
return s;
}

3.读(从磁盘的日志文件中写入到内存的memtable中)
日志读取显然比写入要复杂,要检查checksum,检查是否有损坏等等,处理各种错误。

http://blog.csdn.net/bornshare/article/details/17334071

四 sstable
http://www.cnblogs.com/KevinT/p/3815764.html

http://blog.csdn.net/tankles/article/details/7663905

这里写图片描述
sstable 文件时以块为单位存储的。
1. Datablock,我们知道文件中的k/v对是有序存储的,他们被划分到连续排列的Data Block里面顺序存储起来;
2. 紧跟数据存储区的是Meta Block,存储的是Filter信息,比如Bloom过滤器,用于快速判断key是否在对应数据块;
3. MetaIndex Block是对Meta Block的索引,它只有一条记录,为meta index的名字(也就是Filter的名字)和指向meta Block的BlockHandle;
4. Index block是对Data Block的索引,对于其中的每个记录,其key >=Data Block最后一条记录的key,同时<其后Data Block的第一条记录的key;value是指向data index的BlockHandle;
5) Footer: 元数据的元数据,其中包含Index Block和Meta Index Block的偏移量。Footer之所以在最后,是因为文件生成时是顺序追加的,而Footer的信息又依赖于之前的所有信息,所以只能在最后。由于包含了元数据,所以读取SSTable时首要的就是加载footer。

Data block格式
这里写图片描述
Block data存储的就是我们leveldb中最关键的数据KV对,而type是一个标记Block data是否采用了Snappy压缩算法,crc32顾名思义则是整个block的一个crc校验值,用于判断block是否出错

每一块中包含n条记录

这里写图片描述

Restart表示重启点,num_restarts表重启点个数

每条记录的格式为:

这里写图片描述

SStable中的利器:
1.Bloom过滤器:leveldb的性能的一大利器,bloom.cc
Bitmap就是用一个bit位来标记某个元素对应的Value, 而Key即是该bit的位序。由于采用了Bit为单位来存储数据,因此可以大大节省存储空间。 bitmap通过1个位表示一个状态,比如:int类型有2^32个数字,即4G个数字,那么每个数字一个状态,一位表示一个状态,就是2^32个bit,即512 MB(也就是说,用512兆存储空间就可以处理4G个数据,即40+亿数据)。
Bit-Map方法
建立一个BitSet,将每个key经过一个哈希函数映射到某一位,但是冲突概率大
Bloom Filter算法如下:
创建一个m位BitSet,先将所有位初始化为0,然后选择k个不同的哈希函数。第i个哈希函数对字符串str哈希的结果记为h(i,str),且h(i,str)的范围是0到m-1 。
这里写图片描述

http://www.cnblogs.com/heaad/archive/2011/01/02/1924195.html

2.共享前缀的压缩与重启点
因为Block内容里的KV记录是按照Key大小有序的,所以相邻的两条记录之间的Key很可能存在一个相同的部分,比如key i=“the Car”,Key i+1=“the color”,那么两者存在相同部分“the c”。leveldb就可以利用这个相邻记录存在相同部分来尽量减少Key的存储量,比如Key i+1可以只存储和上一条Key不同的部分“olor”,两者的共同部分从Key i中可以获得。所以整个存储区就存在这样的一个存储情况:一条记录存储完整的Key,而之后的记录开始连续一定的记录数都采取只记载不同的Key部分,然后在是一个重新存储完整的Key值的记录,然后再是一定数量的存储不完整Key的记录,那么我们就称这里的存储完整的Key值的记录为重启点。(leveldb重启点(过大了也不好,原因很简单,获取这个key需要从重启点开始遍历,层数太多)间隔默认是16,同时,Restart Point指向的key也是排序的,可以把底层kv序列的二级索引,在进行key搜索时,先进行Restart Point的二分查找框定范围,然后再在指定的key范围内线性查找)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值