LevelDb-基本数据结构
Slice
实现在slice.cc
class LEVELDB_EXPORT Slice {
...
void clear() {
data_ = "";
size_ = 0;
}
void remove_prefix(size_t n) {
assert(n <= size());
data_ += n;
size_ -= n;
}
private:
const char* data_;
size_t size_;
};
从Slice对外提供的接口看,Slice只是一个容器,不负责理解内容,具体data_对应的内存,释放完全有调用者处理。
使用默认拷贝构造,拷贝复制运算符。
Arena
实现在arena.cc
class Arena {
public;
char* Allocate(size_t bytes);
char* AllocateAligned(size_t bytes);
private:
char* alloc_ptr_;
size_t alloc_bytes_remaining_;
std::vector<char*> blocks_;
std::atomic<size_t> memory_usage_;
简单的内存池,分配的内存在析构函数中回收。对外提供两个内存分配接口,其中一个提供内存对齐能力。
skip list
实现在skiplist.cc
跳表提供的功能:
- 插入,删除,查找元素。
- 有序。支持按区间查找元素。
跳表本质
在链表上实现二分查找。因为不能想连续内存一样直接二分查找,增加索引信息。
时空复杂度
每两个节点作为一个索引:
- 空间复杂度:索引高度O(logn)。跳表的索引高度 h = log2n,且每层索引最多遍历 3 个元素。所以跳表中查找一个元素的时间复杂度为 O(3*logn),省略常数即:O(logn)
- 空间复杂度:O(N)。假如原始链表包含 n 个元素,则一级索引元素个数为 n/2、二级索引元素个数为 n/4、三级索引元素个数为 n/8 以此类推。所以,索引节点的总和是:n/2 + n/4 + n/8 + … + 8 + 4 + 2 = n-2,空间复杂度是 O(n)。
插入,删除数据(如何维护索引)
极端情况分析:不维护索引
慢慢退化成链表
极端情况分析:每次插入都维护
每一级索引都要插入当前元素,时间复杂度O(logn)
插入效率和查找效率取舍
按概率插入到第n级及其以下级索引中。第一级索引概率1/2,第二级概率1/4,以此类推,越高级索引被更新概率越低。
如上图,插入元素6,根据概率需要插入到第2级索引中,这样第3级索引就不用更新了。
删除
每一级索引如果有该元素,都需要删除。
对比红黑树的优势
- 实现简单
- 可以控制n个节点维护一个索引参数,从而做性能和空间trade-off
- 多线程环境下更友好
- 程序局部性更好(如何理解)
leveldb实现
Write:在修改跳表时,需要在用户代码侧加锁。
Read:在访问跳表(查找、遍历)时,只需保证跳表不被其他线程销毁即可,不必额外加锁。
插入
上图演示了两种情况:
- 初始状态,插入key=7的元素。
- 插入key=6的元素前后的状态变化。
对应的代码如下:
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 {
if (prev != nullptr) prev[level] = x;
if (level == 0) {
return next;
} else {
// Switch to next list
level--; //从上往下遍历
}
}
}
}
template <typename Key, class Comparator>
bool SkipList<Key, Comparator>::KeyIsAfterNode(const Key& key, Node* n) const {
// null n is considered infinite
return (n != nullptr) && (compare_(n->key, key) < 0);
}
总体的策略是:
- 两个指针,x指针指向当前节点,next指针指向下个要比较的节点。
- 从左往右遍历。什么情况下结束:遇到比当前key大的节点(NULL指针理解为无限大),对应KeyIsAfterNode方法。
- 从上往下遍历。
找到插入的位置后,需要调整指针:
- 原有节点。比如图中的红色的线。一句话总解就是:新插入节点挡住了原来哪些指针,被挡住的指针都需要调整,指向新插入的节点。
- 插入节点指针。图中绿色的线。
对应代码如下:
int height = RandomHeight();
if (height > GetMaxHeight()) {
for (int i = GetMaxHeight(); i < height; i++) {
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);
}
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);
}
并发处理
LevelDB的跳表实现支持单线程写、多线程读。
怎么理解支持单线程写、多线程读
还是以上面演示的为例,线程1插入key:6,同时线程2和线程3读key:6和key:7。在线程1将key:6完全插入前(指针索引建立完毕前),线程2查不到key:6,线程3能查到key:7。
如果有两个线程同时写数据,该类不保证行为符合预期。
TODO:
return new (node_memory) Node(key) 是什么用法?
为什么泛型变量key可以传0?
memory order
Relaxed ordering
只保证操作原子性(操作变量的指令不进行重排序),不能提供同步机制。
举个例子:
#include <vector>
#include <iostream>
#include <thread>
#include <atomic>
//std::atomic<int> cnt = {0};
int cnt = 0;
void f()
{
for (int n = 0; n < 1000; ++n) {
//cnt.fetch_add(1, std::memory_order_relaxed);
cnt++;
}
}
int main()
{
std::vector<std::thread> v;
for (int n = 0; n < 10; ++n) {
v.emplace_back(f);
}
for (auto& t : v) {
t.join();
}
std::cout << "Final counter value is " << cnt << '\n';
}
执行结果并非预期的10000,每次执行都不一样且小于等于10000。
问题在于i++,在高级语义看来是一个执行单元,但是cpu执行的时候i++是分成多步的。参考如下代码:
void test() {
int cnt = 0;
cnt++;
}
反汇编后
push rbp
mov rbp, rsp
mov dword ptr [rbp - 4], 0
mov eax, dword ptr [rbp - 4] //将cnt变量从内存拷贝到eax寄存器
add eax, 1 //对eax执行+1
mov dword ptr [rbp - 4], eax //将eax寄存器中的值重新拷贝到内存
pop rbp
ret
可以看到i++实际分为三步。在多线程环境下很可能出现如下情况:
thread A: thread B:
mov eax, dword ptr [rbp - 4]
add eax, 1 mov eax, dword ptr [rbp - 4]
mov dword ptr [rbp - 4], eax add eax, 1
mov dword ptr [rbp - 4], eax
假设cnt初值是0,线程A执行第二步add eax, 1后,寄存器eax中值为1,此时线程B开始执行了,将cnt从内存加载到寄存器eax,将线程A的+1后的结果覆盖了。所以最终cnt的值为1,而不是2。
使用std::memory_order_relaxed内存序可保证+1操作原子性。
#include <vector>
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> cnt = {0};
//int cnt = 0;
void f()
{
for (int n = 0; n < 1000; ++n) {
cnt.fetch_add(1, std::memory_order_relaxed);
//cnt++;
}
}
int main()
{
std::vector<std::thread> v;
for (int n = 0; n < 10; ++n) {
v.emplace_back(f);
}
for (auto& t : v) {
t.join();
}
std::cout << "Final counter value is " << cnt << '\n';
}
Relaxed内存序不能保证同步,如下代码可能出现r1 == r2 == 42的情况。
// Thread 1:
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B
// Thread 2:
r2 = x.load(std::memory_order_relaxed); // C
y.store(42, std::memory_order_relaxed); // D
Release-Acquire ordering
CPU为了提高效率使用了指令重排技术,参考如下例子:
// 公共资源
int x = 0;
int y = 0;
int z = 0;
Thread_1: Thread_2:
x = 100; while (y != 200);
y = 200; print x
z = x + y;
x=100;和y=200;这两条指令完全有可能被CPU重排序,先执行y=200;,从线程1内部看没有什么影响,但是在线程2看来,如果未发生重排序x将打印100,如果发生了重排序,x将打印0。
release-acquire内存序可以实现线程间的同步机制。参考下面的例子:
#include <thread>
#include <atomic>
#include <cassert>
#include <string>
#include <iostream>
#include <chrono>
std::atomic<std::string*> ptr;
int data;
void producer()
{
std::string* p = new std::string("Hello"); #1
data = 42; #2
ptr.store(p, std::memory_order_release);
}
void consumer()
{
std::string* p2;
while (!(p2 = ptr.load(std::memory_order_acquire)))
;
assert(*p2 == "Hello"); // never fires #3
assert(data == 42); // never fires #4
}
int main()
{
std::thread t1(producer);
std::thread t2(consumer);
std::thread t3(consumer);
t1.join(); t2.join(); t3.join();
}
一旦consumer线程对ptr使用std::memory_order_acquire load完毕,那么consumer线程能看到producer线程ptr store之前所有写到内存的值(That is, once the atomic load is completed, thread B is guaranteed to see everything thread A wrote to memory)
如例子所示,store 使用 memory_order_release,load 使用memory_order_acquire,CPU 将保证如下两点:
- store 之前的语句不允许被重排序到 store 之后(例子中的 #1 和 #2 语句一定在 store 之前执行)
- load 之后的语句不允许被重排序到 load 之前(例子中的 #3 和 #4 一定在 load 之后执行)
同时 CPU 将保证 store 之前的语句比 load 之后的语句「先行发生」,即先执行 #1、#2,然后执行 #3、#4。这实际上就意味着线程 1 中 store 之前的读写操作对线程 2 中 load 执行后是可见的。
Release-Consume ordering
Release-Consume内存模型和Release-Acquire类似,比Release-Acquire要弱一些,但是更高效。
#include <thread>
#include <atomic>
#include <cassert>
#include <string>
std::atomic<std::string*> ptr;
int data;
void producer()
{
std::string* p = new std::string("Hello"); #1
data = 42; #2
ptr.store(p, std::memory_order_release);
}
void consumer()
{
std::string* p2;
while (!(p2 = ptr.load(std::memory_order_consume)))
;
assert(*p2 == "Hello"); // never fires: *p2 carries dependency from ptr #3
assert(data == 42); // may or may not fire: data does not carry dependency from ptr #4
}
int main()
{
std::thread t1(producer);
std::thread t2(consumer);
t1.join(); t2.join();
}
还是刚才的例子,只是换成了ptr.load(std::memory_order_consume),这时只能保证对ptr的访问#3不会重排序到ptr.load(std::memory_order_consume)之前,但是并不能保证对data的访问不会排序到ptr.load(std::memory_order_consume)之前,所以data不一定是42。
LevelDb中跳表的并发处理
涉及到并发处理一个地方是max_height_字段:
inline int GetMaxHeight() const {
return max_height_.load(std::memory_order_relaxed);
}
max_height_.store(height, std::memory_order_relaxed);
这里只用到了Relaxed内存序。也就是说读写线程没有任何同步机制。假设某个写线程更新了max_height_,分如下两种情况:
- 节点真正插入到链表中了。这种情况显然是正确的。
- 指针关系还没建立好,节点还未插入到链表中。这种情况读线程拿到的height对应的level指针是NULL,查找过程自然会跳过,所以也不影响。
第二个涉及并发处理的是建立索引关系的地方:
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)); #1
prev[i]->SetNext(i, x); #2
}
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);
}
// 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 NoBarrier_SetNext(int n, Node* x) {
assert(n >= 0);
next_[n].store(x, std::memory_order_relaxed);
}
Node* NoBarrier_Next(int n) {
assert(n >= 0);
return next_[n].load(std::memory_order_relaxed);
}
Node的SetNext和Next接口中访问各level指针是通过Release-Acquire内存序来实现的,NoBarrier_SetNext和NoBarrier_Next接口通过Relaxed内存序实现。为什么?
1和#2分别对应下图更新红色指针,和绿色指针的地方。
1处更新绿色指针,更新完后key:6的节点还没串到链表上,更新绿色指针的时候不被读线程读取到也没关系。
2处更新红色指针,更新完后key:6的节点将正式串到链表上,这个时候就需要让读线程能尽快感知到,所以用Release-Acquire内存序来实现。
参考资料:
skip list
memory_order
LevelDb-基本数据结构的更多相关文章
- webpack 4.0.0-beta.0 新特性介绍
webpack 可以看做是模块打包机.它做的事情是:分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其打包为合适的格式 ...
- ssdb底层实现——ssdb底层是leveldb,leveldb根本上是skiplist(例如为存储多个list items,必然有多个item key,而非暴力string cat),用它来做redis的list和set等,势必在数据结构和算法层面上有诸多不适
我已经在用ssdb的hash结构,存储了很多数据了,但是我现在的用法正确吗? 我使用hash结构合理吗? 1. ssdb数据库说是类似redis,而且他们都有hash结构,但是他们的命名有点不同,ss ...
- LevelDB库简介
LevelDB库简介 一.LevelDB入门 LevelDB是Google开源的持久化KV单机数据库,具有很高的随机写,顺序读/写性能,但是随机读的性能很一般,也就是说,LevelDB很适合应用在查询 ...
- HBase、Redis、MongoDB、Couchbase、LevelDB主流 NoSQL 数据库的对比
最近小组准备启动一个 node 开源项目,从前端亲和力.大数据下的IO性能.可扩展性几点入手挑选了 NoSql 数据库,但具体使用哪一款产品还需要做一次选型. 我们最终把选项范围缩窄在 HBase.R ...
- leveldb - log格式
log文件在LevelDb中的主要作用是系统故障恢复时,能够保证不会丢失数据.因为在将记录写入内存的Memtable之前,会先写入Log文件,这样即使系统发生故障,Memtable中的数据没有来得及D ...
- LevelDB源码分析--使用Iterator简化代码设计
我们先来参考来至使用Iterator简化代码2-TwoLevelIterator的例子,略微修改希望能帮助更加容易立即,如果有不理解请各位看客阅读原文. 下面我们再来看一个例子,我们为一个书店写程序, ...
- [转载] leveldb日知录
原文: http://www.cnblogs.com/haippy/archive/2011/12/04/2276064.html 对leveldb非常好的一篇学习总结文章 郑重声明:本篇博客是自己学 ...
- Cassandra——类似levelDB的基于p2p架构的分布式NOSQL数据库
C: Consistency 一致性 • A: Availability 可用性(指的是快速获取数据) • P: Tolerance of network Partition 分区容忍性(分布式) 1 ...
- LevelDb简单介绍和原理——本质:类似nedb,插入数据文件不断增长(快照),再通过删除老数据做更新
转自:http://www.cnblogs.com/haippy/archive/2011/12/04/2276064.html 有时间再好好看下整个文章! 说起LevelDb也许您不清楚,但是如果作 ...
- Leveldb 实现原理
原文地址:http://www.cnblogs.com/haippy/archive/2011/12/04/2276064.html LevelDb日知录之一:LevelDb 101 说起LevelD ...
随机推荐
- iOS 高级面试题
面试题 iOS 基础题 分类和扩展有什么区别?可以分别用来做什么?分类有哪些局限性?分类的结构体里面有哪些成员? 讲一下atomic的实现机制:为什么不能保证绝对的线程安全(最好可以结合场景来说)? ...
- Samsung Wlan AP 默认口令
网络资产搜索: FoFa 进入页面 输入该产品账户密码 在github上面寻找 End!!!
- 程序禁止在 VMware 虚拟机中运行的解决办法
本帖最后由 ibq00 于 2018-12-22 18:54 编辑虚拟机里面不能开游戏!提示这个对话框!Sorry, this application cannot run under a Virtu ...
- 数据库MYSQL常用命令
下载安装命令 Sudo atp-get install xxxx 验证是否安装并启动成功 Sudo netstat -tap | grep xxx 启动 Sudo service mysql star ...
- 第一个知识点:import 和 export
//全部导入import people from './example' //有一种特殊情况,即允许你将整个模块当作单一对象进行导入//该模块的所有导出都会作为对象的属性存在import * as e ...
- nvm在windows下安装与使用
1.卸载本地已经安装的所有node 2.nvm下载 下载地址https://github.com/coreybutler/nvm-windows ,选择nvm-noinstall.zip 放在本地盘, ...
- spring-boot-starter-webflux 与spring-cloud-starter-openfeign冲突
Thu Oct 22 17:16:01 CST 2020 [3be84a1c-14] There was an unexpected error (type=Internal Server Error ...
- js-classList用法学习记录1
classList introduction: 学习后我的个人理解是,在给html中创建的类一系列操作的方法调用. detailed method: 网站具体介绍(菜鸟) add:添加类,已有则不添加 ...
- TRACE()宏的使用
TRACE()宏一般是用在mfc中的,用于将调试信息输出到vs的输出窗口中(这是关键), 这在使用vs作为开发工具的时候,是非常方便的. 然而在开发一般c++程序时,却貌似无法获得这样的便利,其实只要 ...
- MonGdb#Mac安装
1.下载 # 进入 /usr/local cd /usr/local # 下载 sudo curl -O https://fastdl.mongodb.org/osx/mongodb-osx-ssl- ...