跳表
参考资料
Skip List--跳表
特点
- 跳表插入、删除、查找元素的时间复杂度跟红黑树都是一样量级的,时间复杂度都是O(logn)
- 空间换时间,如果每层有1/2的概率保存数据,那么需要2n的空间
- 最底层链表会按顺序保存所有数据,因此区间查找效率高
leveldb跳表实现
节点定义
- Node只有两个成员变量:key和next_。
- next_是一个指针数组,但是是堆栈上申请空间的,因此能够跨界访问,定义为数组的形式方便使用。
- 没有定义数组长度,这是因为数据是按照level从低往高存放的,level-0存放所有数据。访问数据是从高往低,因此不需要知道数组长度。
- 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
- SkipList的构造函数已经设置了head,每层level的链表头都是head
- 每层保存数据的概率为1/4, 也就是说level-n中存在(1/4)^n个数据
- 从低往高存放数据
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
- 查找操作时,prev设置为nullptr
- 查找过程中,都是以第一个小于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--;
}
}
}
}
总结
- SkipList本身支持多线程访问,write操作需要用户代码加锁,read操作无需加锁
- 不支持删除操作(leveldb无需删除操作),能够避免加锁修改,同时避免删除关键节点导致不均衡
- 不需要锁操作的原因
- 单向链表:唯一需要修改的变量就是next_
- 从下往上修改:避免上层出现数据,底层没有数据的情况(next_还是空的)
- 没有删除操作?如果需要加上删除操作,那么需要从上往下修改,应该也是可以实现原子修改的
LRU
参考资料
全面讲解LRU算法
LRU算法
特点
- 最久未使用的数据在缓存个数达到限制时会被删除
- 使用hash表和双向链表来维持数据。插入数据时会将数据插入到双向链表尾,在hash表中保存双向链表的位置,如果数据到达限制,会删除链表头的数据和hash表中的数据;获取数据时,会通过hash表将双向链表中的数据移动到链表尾; 删除数据时删除hash表和双向链表中的数据。时间复杂度都是O(1)。
- 缺点:缓存污染会导致缓存命中率降低,比如文件系统缓存文件项,大量readdir这种只访问一次缓存的操作就会导致缓存失效
leveldb的实现
自实现hash表
- 桶是以双向链表实现的,因此避免桶中查找数据,hash表的桶个数和元素个数相同
- hash中相同key只能存在一个,leveldb的LRU用于缓存文件信息和Block信息,因此没有保存多个相同key的必要
- Resize操作会创建新的桶拷贝旧桶中的指针
- 随机读操作比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
- LRUCache是线程安全的
- 使用lru_(ref=1)和in_use_(ref>1)两个双向链表来保存数据,当缓存个数达到限制时,只能释放lru_中的数据。这个有个问题,如果上层应用如果不调用Release来释放headle,in_use_中的数据是不会释放的,会导致"内存泄漏"
- 使用LRUCache时可能会进行封装,获取headle中的值,然后释放handle。
- 可能处于性能考虑,没有使用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)详解
特点
- 不需要存储key,节省空间
- 通过概率的方式来提高查询效率。通过将key映射为位图(通过几个hash函数),只需记录位图和hash,便能判断key是否存在
- 存在误判,如果位图显示key存在,这个key可能不存在
level实现
- 位图bite和key的个数有关系,bite越多越精确
- 只使用一个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