建议先阅读我的博客 RocksDB 架构
RocksDB 应用于分布式场景,底层通过 LSM - Tree 实现,分布式场景 LSM - Tree 这种结构的原因在于:
- 顺序写:写速度快
- 紧凑型存储:空间占用少,数据压缩比高
1、RocksDB 特性
1.1、列族
列族,CF column famliy
。DB 的每个键值对都与唯一一个列族结合。若不指定列族,则与default
结合
可以这样理解,DB 对应着数据库中的库,CF 对应着数据库中的表。实际上,一个列族可以是一个表,也可以是表的一个索引。一个列族里面可以包括多个表的数据。每个列族有一棵 LSM-Tree 和多个 Memtable。
列族提供了一种数据库逻辑分片的方法
- 支持跨列族的原子写
- 跨列族的一致性视图:快照
- 允许对不同列族进行不同的配置。类比 Mysql 不同的表可以定制不同的存储引擎。
- 可以快速添加 | 删除列族。Mysql DDL 操作代价较高,需要服务器在预启动时创建表的结构,而在分布式场景下,需要即时 DDL 操作。
如图所示:一个DB 的多个列族共享一个 WAL 日志,但是不共享 Memtable 和 SST 文件。通过共享 WAL 日志实现了原子写。通过隔离 Memtable 和 SST 文件,可以独立配置每个列族并且快速删除它们。
当一个列族刷盘时,创建一个新的 WAL 文件。此后,DB 中所有列族都会写入这个 WAL 文件。由于 WAL 共享,所以只有当所有的列族都把这个 WAL 里的数据刷盘了,才能删除该 WAL 文件。
1.2、快照
快照,snapshot
。一个快照会捕获在创建的时间点的 DB 的一致性视图。快照在 DB 重启之后将消失。
一个快照相当于一个 SnapshotImpl
类的对象。其中用于实现 MVCC 的字段是:
number_
:序列号,DB 下全局自增。unix_time_
:DML 操作的时间
快照的本质是双向链表,其应用主要体现在迭代器和事务上。
1.2.1、迭代器
迭代器读取一个指定快照中的数据。
1.2.2、事务
RocksDB 支持事务。
- 悲观事务
TransactionDB
:预写时加锁,适用于高并发写冲突大的场景。例:mysql - 乐观事务
OptimisticTransactionDB
:预写时不加锁,只在提交事务时检查冲突,有冲突就丢弃修改。适用于写冲突小的场景,例:redis
事务的读流程
首先查找本事务对应的 Writebatch 中是否存在请求的数据。接着跳表查找内存的 Memtable(active + immutable );若不存在,则基于 SST 文件元数据查找是否缓存在 Block Cache 中;若没有被缓存,则读磁盘的 SST 文件,找到后并加载到 Block Cache 中。为了提高查找效率,会借助布隆过滤器,避免无效的数据 IO 和遍历操作。
事务的写流程
每个事务都有一个 Writebatch 对象,用于缓存该事务在提交前修改的所有数据。当通过 WriteBatch 写入多个 key 的时候,RocksDB 提供原子化操作。在事务提交时,先写入 WAL 日志,再写入内存可写的 Memtable。当 Memtable 达到阈值后会变为只读的 immutable,此时再新生成一个 Memtable。
数据写入 Memtable 后就意味着事务已经提交。数据的持久化和 compaction 都是异步进行的。当 Immutable Memtable 的数量达到阈值后,会被刷成 L0 SST 文件。在 L0 文件个数达到阈值后,合并到 L1 上并依次往下刷。RocksDB 中可以配置多个线程用于对每层数据文件进行 compaction。
事务的实现方式,见本文第三部分 MyRocks。
2、Pika
pika 是 360 开源的可持久化的大容量类 redis 数据库。
pika 特点
- 容量大,支持百 G 数据量的存储
- 实现 redis 协议。兼容 redis。
- 支持主从
- 数据存储使用 rocksdb
2.1、背景
内存数据库的缺陷:当数据量足够大时,导致内存不够用
- 容量瓶颈:fork 写时复制,内存趋于饱和
- 加载慢:数据库重启,需要加载所有的数据
- 占用带宽:主从复制,在全量复制时,会严重占用带宽
若减少使用 List 命令,pika 可以达到 redis 80% 的性能。
2.2、安装编译
# 安装 lib sudo apt-get install libgflags-dev libsnappy-dev sudo apt-get install libprotobuf-dev protobuf-compiler sudo apt install libgoogle-glog-dev git clone https://github.com/OpenAtomFoundation/pika.git cd pika git submodule update --init git checkout -b v3.4.0 make
使用
./output/bin/pika -c ./conf/pika.conf
2.3、* Blackwidow
Blackwidow 本质是基于 RocksDB 的封装,使只支持 KV 存储的 RocksDB 能够支持多种数据结构。
- Redis 的对象类型对应着 DB
- KV 分离存储分别对应着列族
String
String 由 1 个列族组成,RocksDB 落盘方式:
解释:value 增加了 4 B用于存储 timestamp 用于实现 expire 功能。每当获取一个 string 对象,首先解析 value 后 4 B,获取到 timestamp 后作出判断返回结果(0 - 未设置超时时间)。
Hash
哈希表由两个列族组成
- 元数据
(meta_key, meta_value)
:存储哈希表的信息。 - 普通数据
(data_key, data_value)
:存储对应的 field 和 value。
RocksDB 落盘方式
解释:hash_size 用于实现 hlen 命令。
版本号version
指的是快照的序列号,用于标记删除,真正的删除操作是在 compaction 过程中。元数据中 key 对应的 value 中的 version 记录了最新的有效版本信息
- 若元数据 key 对应的 version 是一个无效版本信息,说明整个数据结构已被删除
- 若普通数据某个字段 field 的 version 大于 key 的 version,说明该字段结点已被删除
由于 String 结构只有一个列族,可以直接删除;对于其他的数据结构,存在多个列族,需要版本号来确定是删除整个 key,还是删除其中的一个 field-value。而对于时间戳timestamp
,只有 key 存在过期时间。
list
list 由两个列族组成:元数据和普通数据。
RocksDB 落盘方式
解释:list_size 实现 llen 命令,left_index 和 right_index 用于实现 lpush 和 rpush 命令。index 实现有序。
Set
list 由两个列族组成:元数据和普通数据。
RocksDB 落盘方式
解释:set_size 实现 scard 命令
Zset
list 由三个列族组成:元数据和普通数据。
RocksDB 落盘方式
解释:第 3 列族用于 zset 排序,按 score 排序,若 score 相等,对 member 来排序。
version 如何用于删除
3、MyRocks
MyRocks 将 RocksDB 替换 InnoDB 作为 Mysql 存储引擎 。
优点:
- 顺序写:写性能更好,但是 InnoDB 读性能远大于 RocksDB。
- 紧凑存储:占用更少的存储空间,具备更高的压缩效率,每一层级的空间浪费控制在 10% 以下;而对于 InnoDB B+ 树来说,若一个节点数据满触发分裂,造成 50% 空间浪费,存在内部碎片。
适用场景
- 大数据量业务。占用更少的存储空间,具备更高的压缩效率
- 写密集业务。rocksdb 顺序写,append 的方式记录 DML 操作,适用于批量插入和更新频繁的业务场景。
3.1、事务实现
事务实现的核心
- 序列号
sequence number
:RocksDB 中的每一条记录都有一个序列号 sn,存储在记录的 key 中 - 快照
snapshot
:快照,与序列号一一对应,对于一个序列号为 sn 的快照来说,只能看到小于或等于 sn 的快照记录,大于 sn 的快照记录不可见
InternalKey:| User key (string) | sequence number (7 bytes) | value type (1 byte) |
隔离级别的实现
- RC:每个 DML 操作,创建 snapshot。每次读取数据时,生成新的 snapshot。
- RR:只有在事务开启时创建 snapshot。没有解决幻读问题,只在主键实现了间隙锁。
MVCC的实现
序列号 sn 提供了记录的多版本信息。当查询记录时,不需要加锁,而是根据当前序列号 sn 创建一个快照,查询过程中只能读取小于或等于 sn 的快照记录,查询结束后释放 snapshot。
- InnoDB 的快照缺陷:undo log 存储所有旧版本的事务,这是因为 undo log 是通过链表实现的,即时有些版本已经没有事务在引用,依然保留,无法删除,占用空间大,读取效率低。
- MyRocks 的快照优化:compaction 操作时会删除中间版本,仅保留当前活跃事务可见版本和记录最新的版本,在满足 MVCC 的基础上,减少了占用空间,读取效率高