LevelDB学习笔记 (2): 整体概览与读写实现细节

1. leveldb整体介绍

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

 LevelDB学习笔记 (2): 整体概览与读写实现细节

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

 LevelDB学习笔记 (2): 整体概览与读写实现细节

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组成,这样可以减少碎片的产生。整体的结构如下图引用自

S4nBu.png

5、Manifest文件(位于磁盘)

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

6、Current文件(位于磁盘)

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

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

LevelDB 结构

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)) {}

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

是对于空字符串的初始化

对于给定长度的c字符串的初始化

对于string的初始化

对于不给定长度的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 个字符。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zzwyxy.html