leveldb工具类分析

跳表

参考资料

Skip List--跳表

特点

  1. 跳表插入、删除、查找元素的时间复杂度跟红黑树都是一样量级的,时间复杂度都是O(logn)
  2. 空间换时间,如果每层有1/2的概率保存数据,那么需要2n的空间
  3. 最底层链表会按顺序保存所有数据,因此区间查找效率高

leveldb跳表实现

节点定义
  1. Node只有两个成员变量:key和next_。
  2. next_是一个指针数组,但是是堆栈上申请空间的,因此能够跨界访问,定义为数组的形式方便使用。
  3. 没有定义数组长度,这是因为数据是按照level从低往高存放的,level-0存放所有数据。访问数据是从高往低,因此不需要知道数组长度。
  4. next_采用了原子修改的方式,是因为SkipList是无锁的
template <typename Key, class Comparator>
struct SkipList<Key, Comparator>::Node {
  explicit Node(const Key& k) : key(k) {}

  Key const key;

  // Accessors/mutators for links.  Wrapped in methods so we can
  // add the appropriate barriers as necessary.
  Node* Next(int n) {
    assert(n >= 0);
    // Use an 'acquire load' so that we observe a fully initialized
    // version of the returned Node.
    return next_[n].load(std::memory_order_acquire);
  }

  void SetNext(int n, Node* x) {
    assert(n >= 0);
    // Use a 'release store' so that anybody who reads through this
    // pointer observes a fully initialized version of the inserted node.
    next_[n].store(x, std::memory_order_release);
  }

  // No-barrier variants that can be safely used in a few locations.
  Node* NoBarrier_Next(int n) {
    assert(n >= 0);
    return next_[n].load(std::memory_order_relaxed);
  }
  void NoBarrier_SetNext(int n, Node* x) {
    assert(n >= 0);
    next_[n].store(x, std::memory_order_relaxed);
  }

 private:
  // Array of length equal to the node height.  next_[0] is lowest level link.
  // Node是堆栈分配的,并且分配了多个Node的大小,所以next_能够跨界访问
  // 将next_设置为数组是为了方便访问和理解
  std::atomic<Node*> next_[1];
};
insert
  1. SkipList的构造函数已经设置了head,每层level的链表头都是head
  2. 每层保存数据的概率为1/4, 也就是说level-n中存在(1/4)^n个数据
  3. 从低往高存放数据
template <typename Key, class Comparator>
void SkipList<Key, Comparator>::Insert(const Key& key) {
  // TODO(opt): We can use a barrier-free variant of FindGreaterOrEqual()
  // here since Insert() is externally synchronized.
  Node* prev[kMaxHeight];
  // prev包含已经存在level中每一层中key之前的节点,用于将key从后面插入
  // 返回level-0层大于等于key的节点,用于查找操作
  Node* x = FindGreaterOrEqual(key, prev);

  // Our data structure does not allow duplicate insertion
  // 判断x==nullptr有什么用?插入第一个节点时,不会assert吗?
  // 上层保证key的单一性(不同的sequnce)
  assert(x == nullptr || !Equal(key, x->key));

  // 所有的节点都存在level-0
  int height = RandomHeight();
  if (height > GetMaxHeight()) {
    // 新增level
    for (int i = GetMaxHeight(); i < height; i++) {
      // 每一层的链表头都是head
      prev[i] = head_;
    }
    // It is ok to mutate max_height_ without any synchronization
    // with concurrent readers.  A concurrent reader that observes
    // the new value of max_height_ will see either the old value of
    // new level pointers from head_ (nullptr), or a new value set in
    // the loop below.  In the former case the reader will
    // immediately drop to the next level since nullptr sorts after all
    // keys.  In the latter case the reader will use the new node.
    max_height_.store(height, std::memory_order_relaxed);
  }

  // 该Node包含height个Node*
  x = NewNode(key, height);
  // 修改每一层的链表
  for (int i = 0; i < height; i++) {
    // NoBarrier_SetNext() suffices since we will add a barrier when
    // we publish a pointer to "x" in prev[i].
    x->NoBarrier_SetNext(i, prev[i]->NoBarrier_Next(i));
    prev[i]->SetNext(i, x);
  }
}
FindGreaterOrEqual
  1. 查找操作时,prev设置为nullptr
  2. 查找过程中,都是以第一个小于key的数作为基准,向下查找
template <typename Key, class Comparator>
typename SkipList<Key, Comparator>::Node*
SkipList<Key, Comparator>::FindGreaterOrEqual(const Key& key,
                                              Node** prev) const {
  Node* x = head_;
  int level = GetMaxHeight() - 1;
  while (true) {
    // 在同一层查找节点(链表需要挨个遍历)
    Node* next = x->Next(level);
    if (KeyIsAfterNode(key, next)) {
      // Keep searching in this list
      x = next;
    } else {
      // x->key < key <= next->key
      if (prev != nullptr) prev[level] = x;
  
      if (level == 0) {
        // 最底层保存了所有节点,返回大于等于key的节点用于查找
        return next;
      } else {
        // x是小于key的
        // Switch to next list
        level--;
      }
    }
  }
}
总结
  1. SkipList本身支持多线程访问,write操作需要用户代码加锁,read操作无需加锁
  2. 不支持删除操作(leveldb无需删除操作),能够避免加锁修改,同时避免删除关键节点导致不均衡
  3. 不需要锁操作的原因
  • 单向链表:唯一需要修改的变量就是next_
  • 从下往上修改:避免上层出现数据,底层没有数据的情况(next_还是空的)
  • 没有删除操作?如果需要加上删除操作,那么需要从上往下修改,应该也是可以实现原子修改的

LRU

参考资料

全面讲解LRU算法
LRU算法

特点

  1. 最久未使用的数据在缓存个数达到限制时会被删除
  2. 使用hash表和双向链表来维持数据。插入数据时会将数据插入到双向链表尾,在hash表中保存双向链表的位置,如果数据到达限制,会删除链表头的数据和hash表中的数据;获取数据时,会通过hash表将双向链表中的数据移动到链表尾; 删除数据时删除hash表和双向链表中的数据。时间复杂度都是O(1)。
  3. 缺点:缓存污染会导致缓存命中率降低,比如文件系统缓存文件项,大量readdir这种只访问一次缓存的操作就会导致缓存失效

leveldb的实现

自实现hash表
  1. 桶是以双向链表实现的,因此避免桶中查找数据,hash表的桶个数和元素个数相同
  2. hash中相同key只能存在一个,leveldb的LRU用于缓存文件信息和Block信息,因此没有保存多个相同key的必要
  3. Resize操作会创建新的桶拷贝旧桶中的指针
  4. 随机读操作比g++-4.4.3快超过5%
桶实现
struct LRUHandle {
  void* value;
  void (*deleter)(const Slice&, void* value);
  LRUHandle* next_hash;  // 相同bucket中下一个LRUHandle
  LRUHandle* next;
  LRUHandle* prev;
  size_t charge;     // TODO(opt): Only allow uint32_t? 
  size_t key_length;
  bool in_cache;     // Whether entry is in the cache.  是否在table_中

  uint32_t refs;     // References, including cache reference, if present.
  uint32_t hash;     // Hash of key(); used for fast sharding and comparisons
  char key_data[1];  // Beginning of key

  Slice key() const {
    // next_ is only equal to this if the LRU handle is the list head of an
    // empty list. List heads never have meaningful keys.
    assert(next != this);

    return Slice(key_data, key_length);
  }
};
hash表实现
class HandleTable {
 public:
  HandleTable() : length_(0), elems_(0), list_(nullptr) { Resize(); }
  ~HandleTable() { delete[] list_; }

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

  // 返回已经存在的相同key的LRUHandle
  LRUHandle* Insert(LRUHandle* h) {
    LRUHandle** ptr = FindPointer(h->key(), h->hash);
    LRUHandle* old = *ptr;
    // 不允许保存多个相同key
    h->next_hash = (old == nullptr ? nullptr : old->next_hash);
    // 修改
    *ptr = h;
    if (old == nullptr) {
      ++elems_;
      // Resize不耗时吗?每次扩展2倍,可能耗时有限(类似std::vector)
      if (elems_ > length_) {
        // 以空间换时间
        // Since each cache entry is fairly large, we aim for a small
        // average linked list length (<= 1).
        Resize();
      }
    }
    return old;
  }

  // 返回删除对象LRUHandle
  LRUHandle* Remove(const Slice& key, uint32_t hash) {
    LRUHandle** ptr = FindPointer(key, hash);
    LRUHandle* result = *ptr;
    if (result != nullptr) {
      // 只修改了当前指针,bucket中前一个对象如何找到下一个对象?
      // 返回的ptr本身就是上一个对象的next_hash的地址, 因此能够对next_hash进行修改
      // pre_obj->next_hash(*ptr)==>cur_obj(**ptr)
      *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_;  // buchet数量
  uint32_t elems_;   // 总的元素个数
  LRUHandle** list_; // bucket中的链表
};
LRUCache
  1. LRUCache是线程安全的
  2. 使用lru_(ref=1)和in_use_(ref>1)两个双向链表来保存数据,当缓存个数达到限制时,只能释放lru_中的数据。这个有个问题,如果上层应用如果不调用Release来释放headle,in_use_中的数据是不会释放的,会导致"内存泄漏"
  3. 使用LRUCache时可能会进行封装,获取headle中的值,然后释放handle。
  4. 可能处于性能考虑,没有使用std::shared_ptr, 而是自实现引用计数。如果使用的std::shared_ptr, 那么每次访问都会进行std::shared_ptr的拷贝。但是在缓存个数达到限制时,可以直接删除std::shared_ptr,严格限制缓存个数
定义
class LRUCache {
 public:
  LRUCache();
  ~LRUCache();

  // Separate from constructor so caller can easily make an array of LRUCache
  void SetCapacity(size_t capacity) { capacity_ = capacity; }

  // Like Cache methods, but with an extra "hash" parameter.
  Cache::Handle* Insert(const Slice& key, uint32_t hash, void* value,
                        size_t charge,
                        void (*deleter)(const Slice& key, void* value));
  Cache::Handle* Lookup(const Slice& key, uint32_t hash);
  // 必须要使用这个接口来释放Insert/Lookup返回的Handle
  void Release(Cache::Handle* handle);
  void Erase(const Slice& key, uint32_t hash);
  // 清空所有未使用的handle
  void Prune();
  size_t TotalCharge() const {
    MutexLock l(&mutex_);
    return usage_;
  }

 private:
  void LRU_Remove(LRUHandle* e);
  void LRU_Append(LRUHandle* list, LRUHandle* e);
  // 从lru_移动到in_use_
  void Ref(LRUHandle* e);
  // 当ref为0时,会被释放; 当ref=1时, 从in_use_移动到lru_
  void Unref(LRUHandle* e);
  bool FinishErase(LRUHandle* e) EXCLUSIVE_LOCKS_REQUIRED(mutex_);

  // capacity_是固定的,usage_计算大小,当usage_>capacity_时需要从lru_中释放数据
  // Initialized before use.
  size_t capacity_;

  // mutex_ protects the following state.
  mutable port::Mutex mutex_;
  size_t usage_ GUARDED_BY(mutex_);

  // LRUHandle只会出现在lru_或者in_use_中,维持着两个链表,这两个链表的数据都在table_中
  // in_use_: 正在使用,指针暴露,调用Prune不能释放
  // lru_: 未被使用,调用Prune能释放
  // Dummy head of LRU list.
  // lru.prev is newest entry, lru.next is oldest entry.
  // Entries have refs==1 and in_cache==true.
  LRUHandle lru_ GUARDED_BY(mutex_);

  // Dummy head of in-use list.
  // Entries are in use by clients, and have refs >= 2 and in_cache==true.
  LRUHandle in_use_ GUARDED_BY(mutex_);

  HandleTable table_ GUARDED_BY(mutex_);
};

布隆过滤器

参考资料

布隆过滤器(Bloom Filter)详解

特点

  1. 不需要存储key,节省空间
  2. 通过概率的方式来提高查询效率。通过将key映射为位图(通过几个hash函数),只需记录位图和hash,便能判断key是否存在
  3. 存在误判,如果位图显示key存在,这个key可能不存在

level实现

  1. 位图bite和key的个数有关系,bite越多越精确
  2. 只使用一个hash函数,减少计算量
class BloomFilterPolicy : public FilterPolicy {
 public:
  explicit BloomFilterPolicy(int bits_per_key) : bits_per_key_(bits_per_key) {
    // We intentionally round down to reduce probing cost a little bit
    k_ = static_cast<size_t>(bits_per_key * 0.69);  // 0.69 =~ ln(2)
    if (k_ < 1) k_ = 1;
    if (k_ > 30) k_ = 30;
  }

  const char* Name() const override { return "leveldb.BuiltinBloomFilter2"; }
  
  // 传入keys列表生成dst二进制字符串
  void CreateFilter(const Slice* keys, int n, std::string* dst) const override {
    // 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;

    // dst保存布隆过滤器构造的结果
    const size_t init_size = dst->size();
    dst->resize(init_size + bytes, 0);
    dst->push_back(static_cast<char>(k_));  // Remember # of probes in filter
    char* array = &(*dst)[init_size];

    // 遍历keys,计算hash,修改指定位置
    for (int i = 0; i < n; i++) {
      // Use double-hashing to generate a sequence of hash values.
      // See analysis in [Kirsch,Mitzenmacher 2006].
      uint32_t h = BloomHash(keys[i]);
      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;
        array[bitpos / 8] |= (1 << (bitpos % 8));
        h += delta;
      }
    }
  }
  
  // 判断key是否是在bloom_filter中
  bool KeyMayMatch(const Slice& key, const Slice& bloom_filter) const override {
    const size_t len = bloom_filter.size();
    if (len < 2) return false;

    const char* array = bloom_filter.data();
    const size_t bits = (len - 1) * 8;

    // Use the encoded k so that we can read filters generated by
    // bloom filters created using different parameters.
    const size_t k = array[len - 1];
    if (k > 30) {
      // 如果布隆过滤器较小直接返回true
      // Reserved for potentially new encodings for short bloom filters.
      // Consider it a match.
      return true;
    }

    // 判断key是否存在
    uint32_t h = BloomHash(key);
    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;
      if ((array[bitpos / 8] & (1 << (bitpos % 8))) == 0) return false;
      h += delta;
    }
    return true;
  }

 private:
  size_t bits_per_key_;  // 每个key多少个bit
  size_t k_;
};
}  // namespace
posted @ 2022-07-23 14:49  nhj11  阅读(156)  评论(0)    收藏  举报