levelDB 的总结

来源:互联网 发布:vue服务端渲染 php 编辑:程序博客网 时间:2024/06/05 08:27

1.内存管理类Arena 的实现:
Arena每次按kBlockSize(static const int kBlockSize = 4096;)单位向系统申请内存,提供地址对齐的内存,记录内存使用。当memtable 申请内存时,如果size小于预留内存,直接使用预留的;如果大于预留,需要重新分配内存块,若需求内存大小小于kBlockSize的四分之一,则分配kBlockSize大小,将剩余的记为预留空间(避免不断的申请小空间),若需求内存大小size大于kBlockSize的四分之一,直接分配size大小。

Arena的关键成员函数就是一个向量vector<char*> blocks_,该向量存放若干个指针,每个指针指向一块内存,结构图如下:

这里写图片描述

class Arena { public:  Arena();  ~Arena();  // Return a pointer to a newly allocated memory block of "bytes" bytes.  char* Allocate(size_t bytes);  // Allocate memory with the normal alignment guarantees provided by malloc  char* AllocateAligned(size_t bytes);  // Returns an estimate of the total memory usage of data allocated  // by the arena.  size_t MemoryUsage() const {    return reinterpret_cast<uintptr_t>(memory_usage_.NoBarrier_Load());  } private:  char* AllocateFallback(size_t bytes);  char* AllocateNewBlock(size_t block_bytes);  // Allocation state  char* alloc_ptr_;  size_t alloc_bytes_remaining_;  // Array of new[] allocated memory blocks  std::vector<char*> blocks_;  // Total memory usage of the arena.  port::AtomicPointer memory_usage_;  // No copying allowed  Arena(const Arena&);  void operator=(const Arena&);};inline char* Arena::Allocate(size_t bytes) {  // The semantics of what to return are a bit messy if we allow  // 0-byte allocations, so we disallow them here (we don't need  // them for our internal use).  assert(bytes > 0);  //如果需求分配小于预留分配大小,直接使用,否则调用AllocateFallback,新分配块  if (bytes <= alloc_bytes_remaining_) {    char* result = alloc_ptr_;    alloc_ptr_ += bytes;    alloc_bytes_remaining_ -= bytes;    return result;  }  return AllocateFallback(bytes);}char* Arena::AllocateFallback(size_t bytes) {//如果新分配的内存大于1/4 kBlockSize,则直接分配bytes,不多分配  if (bytes > kBlockSize / 4) {    // Object is more than a quarter of our block size.  Allocate it separately    // to avoid wasting too much space in leftover bytes.    char* result = AllocateNewBlock(bytes);    return result;  }//如果分配小于1/4 kBlockSize,则直接分配kBlockSize,并记录剩余的大小,记为预留分配  // We waste the remaining space in the current block.  alloc_ptr_ = AllocateNewBlock(kBlockSize);  alloc_bytes_remaining_ = kBlockSize;  char* result = alloc_ptr_;  alloc_ptr_ += bytes;  alloc_bytes_remaining_ -= bytes;  return result;}//字节对齐char* Arena::AllocateAligned(size_t bytes) {  const int align = (sizeof(void*) > 8) ? sizeof(void*) : 8;  assert((align & (align-1)) == 0);   // Pointer size should be a power of 2  size_t current_mod = reinterpret_cast<uintptr_t>(alloc_ptr_) & (align-1);  size_t slop = (current_mod == 0 ? 0 : align - current_mod);  size_t needed = bytes + slop;  char* result;  if (needed <= alloc_bytes_remaining_) {    result = alloc_ptr_ + slop;    alloc_ptr_ += needed;    alloc_bytes_remaining_ -= needed;  } else {    // AllocateFallback always returned aligned memory    result = AllocateFallback(bytes);  }  assert((reinterpret_cast<uintptr_t>(result) & (align-1)) == 0);  return result;}//分配新的内存块char* Arena::AllocateNewBlock(size_t block_bytes) {  char* result = new char[block_bytes];  blocks_.push_back(result);  memory_usage_.NoBarrier_Store(      reinterpret_cast<void*>(MemoryUsage() + block_bytes + sizeof(char*)));  return result;}//构造 析构函数Arena::Arena() : memory_usage_(0) {  alloc_ptr_ = NULL;  // First allocation will allocate a block  alloc_bytes_remaining_ = 0;}Arena::~Arena() {  for (size_t i = 0; i < blocks_.size(); i++) {    delete[] blocks_[i];  }}

2.变长编码
对于一个字符串,存储长度和值,长度如果用int表示,需要4个字节,但数据中很显然大多数都不会有4个字节长度,所以Google采用了变长编码,就是用一个字节的第一个位表示是否数据结束,具体见下:

LevelDB内部通过采用变长编码,对数据进行压缩来减少存储空间,采用CRC进行数据正确性校验。下面就对varint编码进行学习。

传统的integer是以32位来表示的,存储需要4个字节,当如果整数大小在256以内,那么只需要用一个字节就可以存储这个整数,这样就可以节省3个字节的存储空间,Google varint就是根据这种思想来序列化整数的

Varint 是一种紧凑的表示数字的方法。它用一个或多个字节来表示一个数字,值越小的数字使用越少的字节数。这能减少用来表示数字的字节数。

Varint 中的每个 byte 的最高位 bit 有特殊的含义,如果该位为 1,表示后续的 byte 也是该数字的一部分,如果该位为 0,则结束。其他的 7 个 bit 都用来表示数字。因此小于 128 的数字都可以用一个 byte 表示。大于 128 的数字,会用两个字节。

例如整数1的表示,仅需一个字节:

0000 0001

例如300的表示,需要两个字节:

1010 1100 0000 0010
解码:0000010 0101100

采用 Varint,对于很小的 int32 类型的数字,则可以用 1 个 byte 来表示。当然凡事都有好的也有不好的一面,采用 Varint 表示法,大的数字则需要 5 个 byte 来表示。从统计的角度来说,一般不会所有的消息中的数字都是大数,因此大多数情况下,采用 Varint 后,可以用更少的字节数来表示数字信息。

下图演示了 Google Protocol Buffer 如何解析两个 bytes。注意到最终计算前将两个 byte 的位置相互交换过一次,这是因为 Google Protocol Buffer 字节序采用 little-endian(小端) 的方式。

这里写图片描述

这里写图片描述

3.跳表
http://www.cnblogs.com/xuqiang/archive/2011/05/22/2053516.html

这里写图片描述

图一
跳表中不仅有指向下个元素的指针还有随机指向其他后面节点的指针,这样是为了加快搜索速度,搜索的时候从最顶层开始(最底层指向的是相邻的下个元素)

定义每个节点类型:
// 这里仅仅是一个指针
typedef struct nodeStructure *node;
typedef struct nodeStructure

{
keyType key; // key值
valueType value; // value值
// 向前指针数组,根据该节点层数的
// 不同指向不同大小的数组
node forward[1]; //
};
这里写图片描述

插入:
boolean insert(l,key,value)
register list l;
register keyType key;
register valueType value;
{
register int k;
// 使用了update数组
node update[MaxNumberOfLevels];
register node p,q;
p = l->header;
k = l->level;
/**************1步****************/
do {
// 查找插入位置
while (q = p->forward[k], q->key < key)
p = q;

    // 设置update数组,这里记录的是每一层最大的小于这个插入节点的节点,例如图一中要插入20,应该记录的是-1,7,14(从上至下)    update[k] = p;} while(--k>=0);    // 对于每一层进行遍历// 这里已经查找到了合适的位置,并且update数组已经// 填充好了元素

if (q->key == key)
{
q->value = value;
return(false);
};

// 随机生成一个层数
k = randomLevel();
if (k>l->level)
{
// 如果新生成的层数比跳表的层数大的话
// 增加整个跳表的层数
k = ++l->level;
// 在update数组中将新添加的层指向l->header
update[k] = l->header;
};

/**************2步****************/
// 生成层数个节点数目
q = newNodeOfLevel(k);
q->key = key;
q->value = value;

// 更新两个指针域
do
{
p = update[k];
q->forward[k] = p->forward[k];
p->forward[k] = q;
} while(–k>=0);

// 如果程序运行到这里,程序已经插入了该节点

return(true);
}

删除和查找类似,主要是要理解这个update存储的是什么数据。

4.bloom过滤器(原理就是用多个hash函数进行bit-map)
leceldb中首先用一个hash函数将key转换成unit32,再对这个值进行多次的取模处理
用于快速判断key是否在对应的块中,特性:若是检测出不在则一定不在,若是检测出在,可能会检测出在

首先要知道Bit-Map
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 。
这里写图片描述

检查字符串是否存在的过程
对于字符串str,分别计算h(1,str),h(2,str)…… h(k,str)。然后检查BitSet的第h(1,str)、h(2,str)…… h(k,str)位是否为1,若其中任何一位不为1则可以判定str一定没有被记录过。若全部位都是1,则“认为”字符串str存在。若一个字符串对应的Bit不全为1,则可以肯定该字符串一定没有被Bloom Filter记录过。(这是显然的,因为字符串被记录过,其对应的二进制位肯定全部被设为1了)但是若一个字符串对应的Bit全为1,实际上是不能100%的肯定该字符串被Bloom Filter记录过的。(因为有可能该字符串的所有位都刚好是被其他字符串所对应)这种将该字符串划分错的情况,称为false positive 。

Bloom Filter跟单哈希函数Bit-Map不同之处在于:Bloom Filter使用了k个哈希函数,每个字符串跟k个bit对应。从而降低了冲突的概率。

uint32_t Hash(const char* data, size_t n, uint32_t seed) {
// Similar to murmur hash
const uint32_t m = 0xc6a4a793;
const uint32_t r = 24;
const char* limit = data + n;
uint32_t h = seed ^ (n * m);

// Pick up four bytes at a time
while (data + 4 <= limit) {
uint32_t w = DecodeFixed32(data);
data += 4;
h += w;
h *= m;
h ^= (h >> 16);
}

// Pick up remaining bytes
switch (limit - data) {
case 3:
h += static_cast(data[2]) << 16;
FALLTHROUGH_INTENDED;
case 2:
h += static_cast(data[1]) << 8;
FALLTHROUGH_INTENDED;
case 1:
h += static_cast(data[0]);
h *= m;
h ^= (h >> r);
break;
}
return h;
}

static uint32_t BloomHash(const Slice& key) {
return Hash(key.data(), key.size(), 0xbc9f1d34);
}

virtual void CreateFilter(const Slice* keys, int n, std::string* dst) const {
// Compute bloom filter size (in both bits and bytes)
size_t bits = n * bits_per_key_;

// For small n, we can see a very high false positive rate.  Fix it// by enforcing a minimum bloom filter length.if (bits < 64) bits = 64;size_t bytes = (bits + 7) / 8;bits = bytes * 8;const size_t init_size = dst->size();dst->resize(init_size + bytes, 0);dst->push_back(static_cast<char>(k_));  // Remember # of probes in filterchar* array = &(*dst)[init_size];for (int i = 0; i < n; i++) {  // Use double-hashing to generate a sequence of hash values.  // See analysis in [Kirsch,Mitzenmacher 2006]. //从这里就是bloom过滤器的实现了  uint32_t h = BloomHash(keys[i]);//就是一个hash将字符串转换成一个unit32(这个值基本不会相同)  const uint32_t delta = (h >> 17) | (h << 15);  // Rotate right 17 bits  for (size_t j = 0; j < k_; j++) {    const uint32_t bitpos = h % bits;//hash 函数(每次将h+delta,这样就有k个初始值了)    array[bitpos/8] |= (1 << (bitpos % 8));    h += delta;  }}

}
当k很大时,设计k个独立的hash function是不现实并且困难的。对于一个输出范围很大的hash function(例如MD5产生的128 bits数),如果不同bit位的相关性很小,则可把此输出分割为k份。或者可将k个不同的初始值(levelDB就是使用这种方法)结合元素,feed给一个hash function从而产生k个不同的数。

原创粉丝点击