1. leveldb整体介绍

首先leveldb的数据是存储在磁盘上的。采用LSM-Tree实现,LSM-Tree把对于磁盘的随机写操作转换成了顺序写操作。这是得益于此leveldb的写操作非常快,为了做点这一点LSM-Tree的思路是将索引树结构拆成一大一小两棵树,较小的一颗常驻内存,较大的一个持久化到磁盘。而随着内存中的树逐渐增大就会发生树的合并和分裂,大概结构如下图所示。后面还会详细分析

下图是整个leveldb的结构概述图,首先我们会把数据写入memtable(位于内存中),当memtable满了之后。就会变成immutable memtable。也就是所谓的冷却状态,这个时候的memtable无法再被写入数据。在immutable memtable中的数据会准备写入SST(磁盘)中

2. leveldb的主要构成

1、Log文件(位于磁盘)

在我们把数据写Memtable前会先写Log文件,Log通过append的方式顺序写入。Log的存在使得机器宕机导致的内存数据丢失得以恢复。

2、 Memtable(位于内存)

Leveldb主要的内存数据结构,采用跳表进行实现。新的数据会首先写入这里。

3、Immutable Memtable(位于内存)

当Memtable内的数据设置的容量上限后,Memtable会变为Immutable为之后向SST文件的归并做准备。Immutable Mumtable不再接受用户写入,同时生成新的Memtable、log文件供新数据写入。

4、SST文件(位于磁盘)

磁盘数据存储文件。SSTable(Sorted String Table)就是由内存中的数据不断导出并进行Compaction操作后形成的,而且SSTable的所有文件是一种层级结构,第一层为Level 0,第二层为Level 1,依次类推,层级逐渐增高,这也是为何称之为LevelDb的原因。除此之外,Compact动作会将多个SSTable合并成少量的几个SSTable,以剔除无效数据,保证数据访问效率并降低磁盘占用。

SSTABLE是由多个segement组成,这样可以减少碎片的产生。整体的结构如下图引用自

5、Manifest文件(位于磁盘)

Manifest文件中记录SST文件在不同Level的分布,单个SST文件的最大最小key,以及其他一些LevelDB需要的元信息。

6、Current文件(位于磁盘)

从上面的介绍可以看出,LevelDB启动时的首要任务就是找到当前的Manifest,而Manifest可能有多个。Current文件简单的记录了当前Manifest的文件名,从而让这个过程变得非常简单。

上述的整体结构就可以利用下图来描述

3. leveldb读写实现速看

1. 写操作的实现

首先我们通过之前写过的简单的put操作,利用断点跟踪一下整个put过程。

// Write data.
status = db->Put(leveldb::WriteOptions(), "name", "zxl");

WriteOptions 控制着我们是否需要 sync,也就是刷到磁盘上

根据断点执行,上述的put操作要先进入db/db_impl.cc里面的Put函数。这里可以发现的key和value都是以Slice形式来存储,也就是切片来存储。因此在往下追溯之前我们先来看一下切片

// Convenience methods
Status DBImpl::Put(const WriteOptions& o, const Slice& key, const Slice& val) {
return DB::Put(o, key, val);
}

切片的实现

切片的整体代码位于include/Slice.h。整体由一个类组成。其中含有一个c字符串和一个大小变量

class LEVELDB_EXPORT Slice {
// ......
private:
const char* data_;
size_t size_;
}

整体关于Slice的构造函数有几种不同的重载,下面我们仔细来看一下

// Create an empty slice.
Slice() : data_(""), size_(0) {} // Create a slice that refers to d[0,n-1].
Slice(const char* d, size_t n) : data_(d), size_(n) {} // Create a slice that refers to the contents of "s"
Slice(const std::string& s) : data_(s.data()), size_(s.size()) {} // Create a slice that refers to s[0,strlen(s)-1]
Slice(const char* s) : data_(s), size_(strlen(s)) {}

上面四种分别对应了不同的情况

  1. 是对于空字符串的初始化
  2. 对于给定长度的c字符串的初始化
  3. 对于string的初始化
  4. 对于不给定长度的c字符串的初始化

对于拷贝构造和拷贝赋值则都采用了c++11的默认方法=default来实现

关于c++11的默认拷贝构造与拷贝赋值

// Intentionally copyable.
Slice(const Slice&) = default; // 默认浅拷贝
Slice& operator=(const Slice&) = default;

同样关于切片还有一些特殊作用的函数,来分析一下

starts_with函数用来判断x是不是当前Slice的一个前缀。这里用到了memcmp这个c语言库函数。

int memcmp (const void *s1, const void *s2, size_t n); 用来比较s1 和s2 所指的内存区间前n 个字符。

如果返回值为0则表示相同,否则会返回差值,这里是按照ascll的顺序来进行比较的

 // Return true iff "x" is a prefix of "*this"
bool starts_with(const Slice& x) const {
return ((size_ >= x.size_) && (memcmp(data_, x.data_, x.size_) == 0));
}

下面还有两个切片的比较函数

  1. 假如说我们调用a.compare(b)
  2. 那么比较的逻辑就是先看a和b谁比较长一点。
  3. 然后取较小的长度去进行比较
  4. 如果在较小的长度内a和b是相同的,那么就是谁长谁就更大
  5. 如果在较小的长度内a和b不是想同的,那么就以较小长度内的比较为准
inline int Slice::compare(const Slice& b) const {
const size_t min_len = (size_ < b.size_) ? size_ : b.size_;
int r = memcmp(data_, b.data_, min_len);
if (r == 0) {
if (size_ < b.size_)
r = -1;
else if (size_ > b.size_)
r = +1;
}
return r;
}

写一个测试代码

 auto a =  new leveldb::Slice("123");
leveldb::Slice b;
b = leveldb::Slice("21");
std:: cout << "a 与 b 的比较结果" << a->compare(b) << std::endl;

上述代码会输出-1表示a < b这符合我们的预期

好了我们上面知道了key和value都是以切片的形式进行存储的。ok下面继续分析写操作

随后进入db/db_imp.cc中的DB::Put函数

// Default implementations of convenience methods that subclasses of DB
// can call if they wish
Status DB::Put(const WriteOptions& opt, const Slice& key, const Slice& value) {
WriteBatch batch;
batch.Put(key, value);
return Write(opt, &batch);
}

这里会定义一个writeBatch来进行写入操作,它会调用batch.put来实现原子写。

不过我们前面有说写操作会先写Log来防止出现意外。而数据则会先写入memtable

Write Log操作

在调用write(opt, &batch)的时候

Status DBImpl::Write(const WriteOptions& options, WriteBatch* updates) {
Writer w(&mutex_);
w.batch = updates;
w.sync = options.sync;
w.done = false; MutexLock l(&mutex_);
writers_.push_back(&w);
// 排队写入,直到我们在 front
while (!w.done && &w != writers_.front()) {
w.cv.Wait();
}
if (w.done) {
return w.status;
}
{
mutex_.Unlock();
status = log_->AddRecord(WriteBatchInternal::Contents(write_batch));

上述操作就是写入log的操作。而下面的代码则是写入memTable

 if (status.ok()) {
status = WriteBatchInternal::InsertInto(write_batch, mem_); // 这里写入,mem_ 是 MemTable,
}

关于log写入的具体分析后面会在log详解的时候分析

写入memtable

Status WriteBatchInternal::InsertInto(const WriteBatch* b, MemTable* memtable) {
MemTableInserter inserter;
inserter.sequence_ = WriteBatchInternal::Sequence(b);
inserter.mem_ = memtable;
return b->Iterate(&inserter);
}

上面的代码最终会执行到。memtable.cc

void MemTable::Add(SequenceNumber s, ValueType type, const Slice& key,
const Slice& value) {
// Format of an entry is concatenation of:
// key_size : varint32 of internal_key.size()
// key bytes : char[internal_key.size()]
// value_size : varint32 of value.size()
// value bytes : char[value.size()]
size_t key_size = key.size();
size_t val_size = value.size();
size_t internal_key_size = key_size + 8;
const size_t encoded_len = VarintLength(internal_key_size) +
internal_key_size + VarintLength(val_size) +
val_size;
// 为要put的key value 创建空间
char* buf = arena_.Allocate(encoded_len);
char* p = EncodeVarint32(buf, internal_key_size);
// copy进去
std::memcpy(p, key.data(), key_size);
p += key_size;
EncodeFixed64(p, (s << 8) | type);
p += 8;
p = EncodeVarint32(p, val_size);
std::memcpy(p, value.data(), val_size);
assert(p + val_size == buf + encoded_len);
// 将索引写入SkipList
table_.Insert(buf);
}

前面说过当memtable满了之后会写入磁盘也就是sstable。对应的代码在MakeRoomForWrite后面再仔细分析了

Status status = MakeRoomForWrite(updates == nullptr);
uint64_t last_sequence = versions_->LastSequence();

2. 读操作的实现

同样的还是通过debug的方式追踪代码

代码位于db_impl.cc:DBImpl::Get

// Unlock while reading from files and memtables
{
mutex_.Unlock();
// First look in the memtable, then in the immutable memtable (if any).
LookupKey lkey(key, snapshot);
if (mem->Get(lkey, value, &s)) {
// Done
} else if (imm != nullptr && imm->Get(lkey, value, &s)) {
// Done
} else {
s = current->Get(options, lkey, value, &stats);
have_stat_update = true;
}
mutex_.Lock();
} if (have_stat_update && current->UpdateStats(stats)) {
MaybeScheduleCompaction();
}
mem->Unref();
if (imm != nullptr) imm->Unref();
current->Unref();
return s;

可以发现读操作会先根据key值做一个查找操作loocupKey

随后去memtable中查找。如果memtable没有则会去 immutable中寻找

如果上面两个地方都查不到的话最后则要去sstable中查找。

4. 总结

借鉴了很多大佬们的资料, 分析了一下leveldb的整体结构,以及读和写操作的简单实现,当然后面还会进一步分析。更详细的讲解读和写操作的实现。下一篇就分析一下memtable的实现以及是如何写入memtableimmutable的。

5. 参考资料

https://youjiali1995.github.io/rocksdb/io/

https://github.com/google/leveldb/tree/v1.20/doc

http://catkang.github.io/2017/01/07/leveldb-summary.html

https://www.zhihu.com/column/c_1327581534384230400

https://www.youtube.com/watch?v=PSna05F5fL4&list=PLBokfyNIQPIR2EOXnpemSqzXkkZylAmsD

http://blog.yanick.site/2020/11/08/algorithm/lsm-tree/

https://mp.weixin.qq.com/s/RmyBUUrNVUrmHBJ-7ujM3w

LevelDB学习笔记 (2): 整体概览与读写实现细节的更多相关文章

  1. LevelDB学习笔记 (3): 长文解析memtable、跳表和内存池Arena

    LevelDB学习笔记 (3): 长文解析memtable.跳表和内存池Arena 1. MemTable的基本信息 我们前面说过leveldb的所有数据都会先写入memtable中,在leveldb ...

  2. LevelDB学习笔记 (1):初识LevelDB

    LevelDB学习笔记 (1):初识LevelDB 1. 写在前面 1.1 什么是levelDB LevelDB就是一个由Google开源的高效的单机Key/Value存储系统,该存储系统提供了Key ...

  3. LevelDB 学习笔记2:合并

    LevelDB 学习笔记2:合并 部分图片来自 RocksDB 文档 Minor Compaction 将内存数据库刷到硬盘的过程称为 minor compaction 产出的 L0 层的 sstab ...

  4. LevelDB 学习笔记1:布隆过滤器

    LevelDB 学习笔记1:布隆过滤器 底层是位数组,初始都是 0 插入时,用 k 个哈希函数对插入的数字做哈希,并用位数组长度取余,将对应位置 1 查找时,做同样的哈希操作,查看这些位的值 如果所有 ...

  5. Dynamic CRM 2013学习笔记(十七)JS读写各种类型字段方法及技巧

    我们经常要对表单里各种类型的字段进行读取或赋值,下面列出各种类型的读写方法及注意事项: 1. lookup 类型 清空值 var state = Xrm.Page.getAttribute(" ...

  6. AntDesign vue学习笔记(七)Form 读写与图片上传

    AntDesign Form使用布局相比传统Jquery有点繁琐 (一)先读写一个简单的input为例 <a-form :form="form" layout="v ...

  7. Bootstrap学习笔记之整体架构

    之前有粗略地看过一下Bootstrap的内容,不过那只是走马观花式地看下是怎么用的,以及里面有什么控件,所以就没想着记笔记.现在由于要给部门做分享,所以不得不深入地去学习下,不然仅是简单地说下怎么用, ...

  8. leveldb学习笔记

    LevelDB由 Jeff Dean和Sanjay Ghemawat开发. LevelDb是能够处理十亿级别规模Key-Value型数据持久性存储的C++ 程序库. 特别如下: 1.LevelDb是一 ...

  9. leveldb 学习笔记之VarInt

    在leveldb在查找比较时的key里面保存key长度用的是VarInt,何为VarInt呢,就是变长的整数,每7bit代表一个数,第8bit代表是否还有下一个字节, 1. 比如小于128(一个字节以 ...

随机推荐

  1. WIKI和JIRA-安装与使用

    1.Wiki介绍1.1 Wiki(多人协作的写作系统)是一种超文本系统,这种超文本系统支持面向社群的协作式写作,即人人可编辑.在公司的项目管理中,可以把它当作文档管理和信息组织(Portlet)系统来 ...

  2. [刷题] 20 Valid Parentheses

    要求 给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效 左括号必须用相同类型的右括号闭合 左括号必须以正确的顺序闭合 空字符串可被认为是有效字符串 思路 遇 ...

  3. [Java] Structs

    背景 基于MVC的WEB框架 在表示层过滤访问请求并处理 步骤 在eclipse中创建Web动态项目 导入相关jar包到WEB-INF/lib 在WEB-INF目录下新建web.xml,配置Filte ...

  4. [Python] 爬虫系统与数据处理实战 Part.1 静态网页

    爬虫技术基础 HTTP/HTTPS(7层):应用层,浏览器 SSL:加密层,传输层.应用层之间 TCP/IP(4层):传输层 数据在传输过程中是加密的,浏览器显示的是解密后的数据,对爬虫没有影响 中间 ...

  5. Iperf3网络性能测试工具详解教程

    Iperf3网络性能测试工具详解教程 小M 2020年4月17日 运维 本文下载链接 [学习笔记]Iperf3网络性能测试工具.pdf 网络性能评估主要是监测网络带宽的使用率,将网络带宽利用最大化是保 ...

  6. Django/Flask的一些实现方法

    一.导出当前项目用到的依赖到requirements.txt文件中 pip freeze > requirements.txt 二.安装当前项目需要的依赖: pip install -r req ...

  7. Linux——CentOS7添加/删除用户和用户组1

    Linux--CentOS7添加/删除用户和用户组 2017.05.02 19:58 23012浏览   前言 今天又重新装了centos7突然有关用户和用户组有关的命令记不清了,所以记一下,也方便你 ...

  8. MQTT简介-(转自cacard)

    MQTT - MQ Telemetry Transport   轻量级的 machine-to-machine 通信协议. publish/subscribe模式. 基于TCP/IP. 支持QoS. ...

  9. 11.14 mii-tool:管理网络接口的状态

    mii-tool命令用于查看.管理网络接口,默认情况下网卡的状态是自动协商的,但是有时也会出现不正常的情况,可以使用mii-tool进行调整. mii-tool [option] [interface ...

  10. 灵动微电子ARM Cortex M0 MM32F0010 GPIO 的配置驱动LED灯

    灵动微电子ARM Cortex M0 MM32F0010 GPIO的配置 目录: 1.前言 2.学习方法简要说明 3.要点提示 4.注意事项 5.MM32F0010系统时钟的配置 6.MM32F001 ...