Open Source, Open Future!
  menu
107 文章
ღゝ◡╹)ノ❤️

RocksDB

1、是什么

  • 是一个使用C++编写的嵌入式 KV 存储引擎。
  • 是Facebook基于levelDB开发的。

2、特点

高性能

RocksDB使用一套日志结构的数据库引擎,为了更好的性能,这套引擎是用C++编写的。 Key和value是任意大小的字节流。

为快速存储而优化

RocksDB为快速而又低延迟的存储设备(例如闪存或者高速硬盘)而特殊优化处理。 RocksDB将最大限度的发挥闪存和RAM的高度率读写性能。

可适配性

RocksDB适合于多种不同工作量类型。 从像MyRocks这样的数据存储引擎, 到应用数据缓存, 甚至是一些嵌入式工作量,RocksDB都可以从容面对这些不同的数据工作量需求。

基础和高级的数据库操作

RocksDB提供了一些基础的操作,例如打开和关闭数据库。 对于合并和压缩过滤等高级操作,也提供了读写支持。

3、发展状况

目前,业界使用或支持RocksDB的项目:

  • JIMKV

京东自研的分布式数据库,支持RocksDB存储引擎。

  • CockroachDB

网易开源的分布式New-SQL数据库,底层使用Rocksdb。

  • ByteKV

字节跳动自研的 KV 存储系统。底层使用了Rocksdb 存储引擎。

  • Pika

360自研的类Redis存储系统。存储引擎是基于 Rocksdb 修改的。

  • Kvrocks

美图开源的一个KV数据库,基于RocksDB并与Redis协议兼容。

  • Parker

OPPO 自研的一个基于 RocksDB 的分布式 KV 存储系统。

  • WTable

58自研的分布式KV存储系统。

  • TiKV

PingCAP 开源的一个支持分布式事务和水平扩展的 KV 数据库。底层本地存储是依赖了 RocksDB。

  • LogDevice

Facebook 开源的一个用于日志的分布式数据存储系统。它的本地日志存储被称为LogsDB,是构建于RocksDB之上的。

4、示例

4.1、引入依赖

<dependency>
    <groupId>org.rocksdb</groupId>
    <artifactId>rocksdbjni</artifactId>
    <version>6.5.3</version>
</dependency>

4.2、连接数据库

@Before
public void init() throws RocksDBException {
    Options options = new Options().setCreateIfMissing(true);
    db = RocksDB.open(options, dbPath);
}

4.3、插入数据

@Test
public void testPut() throws RocksDBException {
    db.put("hello".getBytes(), "world".getBytes());
    db.put("你好".getBytes(), "世界".getBytes());
}

4.4、查询数据

@Test
public void testGet() throws RocksDBException {
    System.out.println(new String(db.get("hello".getBytes())));
    System.out.println(new String(db.get("你好".getBytes())));
}

4.5、删除数据

@Test
public void testRemove() throws RocksDBException {
    System.out.println(db.get("hello".getBytes()) != null);
    db.remove("hello".getBytes());
    System.out.println(db.get("hello".getBytes()) != null);
}

4.6、批量插入

@Test
public void testMPut() throws RocksDBException {
    WriteOptions writeOpt = new WriteOptions();
    WriteBatch batch = new WriteBatch();
    for (int i = 0; i < 5; i++) {
        batch.put(("testMPut_" + i).getBytes(), String.valueOf(i).getBytes());
    }
    db.write(writeOpt, batch);
}

4.7、批量查询

@Test
public void testMGet() throws RocksDBException {
    List<byte[]> keys = new ArrayList<>();
    for (int i = 0; i < 5; i++) {
        keys.add(("testMPut_" + i).getBytes());
    }
    List<byte[]> values = db.multiGetAsList(keys);
    for (int i = 0; i < keys.size(); i++) {
        System.out.println(new String(keys.get(i)) + "--" + (values.get(i) == null ? null : new String(values.get(i))));
    }
}

4.8、迭代器

正序:

@Test
public void testIterator1() {
    RocksIterator iterator = db.newIterator();
    // 从第一个元素开始,正序遍历
    for (iterator.seekToFirst(); iterator.isValid(); iterator.next()) {
        System.out.println(new String(iterator.key()) + "--" + new String(iterator.value()));
    }
}

倒序:

@Test
public void testIterator2() {
    RocksIterator iterator = db.newIterator();
    // 从最后一个元素开始,倒序遍历
    for (iterator.seekToLast(); iterator.isValid(); iterator.prev()) {
        System.out.println(new String(iterator.key()) + "--" + new String(iterator.value()));
    }
}

从指定元素开始:

@Test
public void testIterator3() {
    RocksIterator iterator = db.newIterator();
    for (iterator.seek("testMPut_1".getBytes()); iterator.isValid(); iterator.next()) {
        System.out.println(new String(iterator.key()) + "--" + new String(iterator.value()));
    }
}

5、架构

6、模块

6.1、log文件

描述:

写Memtable前会先写Log文件,Log通过append的方式顺序写入。

作用:

Log的存在使得机器宕机导致的内存数据丢失得以恢复。

格式:

说明:

  • log 文件由多个 32 KB的 block 组成
  • 每个 block 由多个 record 组成(若末尾不足7字节,用0x00填充;若末尾刚好 7 字节,用一个 length 为 0 的 record填充)。
  • 每个 record 由 header 和 data 组成
  • chechSum:校验码,判断数据是否被破坏
  • length:数据的大小
  • type:当数据很大时,可能会存储在多个block中;用来表示record 的类型

record类型:

type说明
FULL表示当前 record 完整地存储在一个 block 里
FIRST表示这是一个 record 的起始部分
MIDDLE表示这是一个record 的中间部分
LAST表示这是一个 record 的末尾部分

6.2、memtable

6.3、immutable memtable

  • 当memtable的size达到阈值,会变成immutable memtable
  • immutable memtable与memtable结构是一样的,只提供读不允许写入
  • 由后台线程 flush 到磁盘上

6.4、SSTable

全称:

Sorted String Table

描述:

  • 分为 level-0 到 level-n 多层(最大支持7层)
  • 每一层包含多个 SSTable
  • 文件内数据是有序
  • 除了 level-0 之外,每一层内部的 SSTable 的 key 范围都不相交
  • 默认大小2MB

结构:

说明:

  • Footer:固定 48 字节,位于文件尾部,索引的索引
  • Index Block:data block的索引
  • Meta Index Block:meta block的索引,目前 LevelDB 中只有一个 meta block
  • meta block:filter,默认是bloom filter
  • Data Block:存储实际的 key-value 数据,默认大小4KB。
6.4.1、Footer

结构:

说明:

  • meta index handle :指向 meta index block
  • index handle: 指向 index block
  • padding:填充补齐
  • magic number:魔数, 8 个字节
  • handle 由offset 和 size 组成
  • offset 和 size 最少占用1个字节长度、最多占用9个字节长度
6.4.2、Index Block

结构:

说明:

索引分为三部分:

  • 记录大于等于block i 中最大的Key且小于block i +1中最小的Key的 key
  • block i 在sst文件中的起始位置
  • block i 的大小
6.4.3、Data Block

结构:

说明:

  • Record:实际的KV记录,按顺序排列
  • Restart:重启点
6.4.4、重启点

解决了什么问题?

Block里的记录key是有序的,所以,相邻的两条记录很可能Key部分存在重叠,从节省空间的角度考虑,后面的key可以只存储跟前面不同的部分,相同的部分可以从前面的数据获取。

描述:

重启点表示以当前记录为新的基础,后面的数据只存不同的部分。

6.4.5、Record

结构:

6.5、Manifest文件

保存了整个 LevelDB 实例的元数据:

  • 每一层有哪些 SSTable
  • 单个SSTable文件的文件大小、最大 key、最小 key 等信息
  • 其他一些LevelDB需要的元信息

6.6、Current文件

Current文件简单的记录了当前Manifest的文件名

7、写流程

当写入一条Key:Value时,流程如下:

  1. 先往log文件里写入
  2. 成功后将记录插进Memtable中

这样就完成了一次写入操作,只涉及一次磁盘顺序写和一次内存插入,这也是写入速度很快的原因。

8、读流程

流程:

  1. 先去查询 Memtable,找到则返回
  2. 再去查询 Immutable Memtable,找到则返回
  3. 查询 level 0 的文件level 0 比较特殊, level 0 下的不同文件可能key的范围有重叠,查询流程如下:3.1. 根据manifest的记录,找到相关的所有文件3.2. 对文件排序,从最新的文件依次查询,直到找到为止,找到返回,找不到去下一层找3.2.1. 对于每个sst文件,查询文件的索引(index block)3.2.1.1. 若缓存(Table Cache )中有,用缓存中的索引3.2.1.2. 若缓存中没有,则打开sst文件,同时将这个文件的索引部分加载进缓存中****3.2.2. 根据索引找对应的数据(data block)3.2.2.1. 若缓存(Block Cache )中有,用缓存中的数据3.2.2.2. 若缓存中没有,则从文件中查询,并放入缓存
  4. 查询 level i(i>0) 的文件****不同文件key不重复,所以最多只对应一个文件,后面的流程与 3.2.1 ~3.2.2相同

其中3.2的流程图如下:

9、删除

LevelDB的删除操作是标记删除,并没有真正删除原来的数据,真正的删除操作通过compaction删除

10、缓存

有两个cache是为了优化读性能的:TableCache和BlockCache。这两种Cache都采用了LRUCache机制,只是内部存储的数据类型不同。

10.1、TableCache

用于缓存 SSTable 的文件描述符、索引和 filter

10.2、BlockCache

SSTable 的数据是被组织成一个个 block。BlockCache 用于缓存这些 block(解压后)的数据。

11、压缩

levelDb采取了compaction的方式来对已有的记录进行整理压缩,通过这种方式,来删除掉一些不再有效的KV数据,减小数据规模,减少文件数量等。LevelDB Compaction分两种:

  • minor compaction
  • major compaction

11.1、minor compaction

当内存中的memtable大小到了一定值时,将内容保存到磁盘文件中:

主要做两件事情:

  1. 构造sstable
  2. 新的sstable文件写入哪一层

流程如下:

新产生出来的sstable 并不一定总是处于level 0, 尽管大多数情况下,处于level 0。新创建的出来的sstable文件应该位于那一层呢? 由PickLevelForMemTableOutput 函数来计算。

11.1、major compaction

当某个level下的SSTable文件数目超过一定设置值后,levelDb会从这个level的SSTable中选择一个文件(level>0),将其和高一层级的level+1的SSTable文件合并,这就是major compaction。

12、扩展

12.1、LSM-Tree

全称:

Log-Structured-Merge-Tree

起源:

1996 年的一篇论文《The Log-Structured Merge-Tree (LSM-Tree)》。

思想:

  • 新增数据先记录 log
  • 在保存进内存中,超过阈值后,批量flush到磁盘
  • 磁盘数据达到一定条件后merge,优化读性能

特点:

写入速度很快,主要利用了磁盘的顺序写。

12.2、对比LevelDB

  • 多MemTable,防止可能出现的写阻塞
  • 多线程压缩合并
  • 将flush 和 compaction分开用不同的线程池来进行调度
  • 增加了对WAL的特殊管理机制
  • 增加了merge operator,即原地更新
  • 增加了ColumnFamily(列族)
  • 支持分层自定义压缩算法
  • 性能更好
  • 适应更多的应用场景