RocksDB的特性及其应用

RocksDB的特性及其应用

一 RocksDB的特性

1 列族

列族相当于关系型数据库中的表,一个列族独有一个LSM-TREE,但是一个DB中的所有LSM-TREE共享一个WAL。RocksDB的每个键值对都与唯一一个列族(column family)结合。如果没有指定 ColumnFamily,键值对将会结合到 “default” 列族。

列族的主要实现思想是他们共享一个 WAL 日志,但是不共享 memtable 和 table 文件。通过共享
WAL 文件,我们实现了原子写。通过隔离 memtable 和 table 文件,我们可以独立配置每个列族并且快速删除它们。

每当一个单独的列族刷盘,我们创建一个新的 WAL 文件。所有列族的所有新的写入都会去到新的WAL文件。但是,我们还不能删除旧的 WAL,因为他还有一些对其他列族有用的数据。我们只能在所有的列族都把这个WAL 里的数据刷盘了,才能删除这个 WAL 文件。

2 快照

一个快照会捕获在创建的时间点的 DB 的一致性视图。快照在DB重启之后将消失。它只持有部分简单的字段,比如快照生成时候的number_ 以及 unix_time_ 。

Snapshot 会存储在一个 DBImpl 持有的链表里,且是是顺序排列。快照具有伸缩性的问题;因为使用链表。尽管他是顺序排列的,他还是不可以使用二分查找。在flush/comapction 的时候,如果我们需要找到一个 key 在最早的哪个 snapshot 中是可见的,我们必须逐个扫描快照链表。当许多快照存在的时候,这个扫描会非常明显的拖慢flush/compaction 进程。当有成百上千个快照的时候就能观察到这种问题了。

3 迭代器

如果 ReadOptions.snapshot 被给出,那么迭代器会从一个快照里面返回数据。如果这是一个
nullptr,迭代器隐式创建一个迭代器创建的时间节点的快照,利用该隐式快照提供数据。隐式快
照无法转换为显式快照。

4 事务

RocksDB 将支持事务。事务带有简单的BEGIN/COMMIT/ROLLBACK API,并且允许应用并发地修改数据,具体的冲突检查,由RocksDB 来处理。RocksDB 支持悲观和乐观的并发控制。

  1. 悲观事务
    悲观事务默认写会有大量的冲突。当使用悲观事务的时候,所有正在修改的 RocksDB 里的 key 都会被上锁,让 RocksDB 执行冲突检测。如果一个 key 发生锁冲突,操作会返回一个错误。当事务被提交,数据库保证这个事务是可以写入的。
  2. 乐观事务
    乐观事务默认写不会有大量的冲突。乐观事务在预备写的时候不使用任何锁。作为替代,他们把这个操作推迟到在提交的时候检查,是否有其他人修改了正在进行的事务。如果和另一个写入有冲突(或者他无法做决定),提交会返回错误,并且任何 key 都不会被写入。

在有大量并发写的工作压力的时候,悲观事务比较合适,否则乐观事务比较合适。

5 底层实现

读快照:
每一个 RocksDB 的更新都是通过插入一个带有强制自增的序列号的项来实现的。给即将要被(事务或者非事务)DB 用来创建快照的 read_options。snapshot赋值一个序列号,可以只读到小于这个序列号的内容。
读写冲突检测:
读写冲突可以通过升级为写写冲突来防止:通过 GetForUpdate (而不是Get )来做读操作。
写写冲突检测(悲观):
写写冲突在写入的时候用一个锁表来进行检测。非事务更新(put,merge,delete)在内部其实
是以一个事务来运行的。
写写冲突检测(乐观):
写写冲突会在提交的时候检查其最后一个序列号,来检测冲突。
冲突检测的逻辑在 TransactionUtil::CheckKeysForConflicts 中实现;
只检测在内存中出现的 key 的冲突和失败。
冲突检测是通过比对每个 key 的最后的序列号( DBImpl::GetLatestSequenceForKey )与
用于写入的序列号来实现的。

二 Pika

Pika是一种需要存储大量数据时代替redis的一种解决方案。兼容 string、hash、list、zset、set 的绝大部分接口,解决 Redis 由于存储数据量巨大而导致内存不够用的容量瓶颈,并且可以像 Redis 一样,通过 slaveof 命令进行主从备份,支持全同步和部分同步,Pika 还可以用在 twemproxy 或者codis 中来实现静态数据分片;

如果需要存储的数据大于50G的情况下,最好使用Pika取代redis。Pika的运行效率是redis的80%左右。

RocksDB的特性及其应用_第1张图片
图中的BlackWidow的作用就是利用键值对实现redis的数据结构。在具体的实现上,Pika中的每一种库,对应一个redis中的数据结构。

Blackwidow 本质上是基于 RocksDB 的封装,使本身只支持 kv 存储的 RocksDB 能够支持多种数据结构,目前 Blackwidow 支持五种数据结构的存储:String 结构(实际上就是存储 key,value),Hash 结构,List 结构,Set 结构和 ZSet 结构, 因为 RocksDB 的存储方式只有 kv 一种, 所以上述五种数据结构最终都要落盘到 RocksDB 的 kv 存储方式上。

String 结构的存储
String 本质上就是 Key,Value,我们知道 RocksDB 本身就是支持 kv 存储的,为了实现 Redis 中的 expire 功能,所以在 value 后面添加了 4 Bytes 用于存储 timestamp,作为最后 RocksDB 落盘的 kv 格式。
在这里插入图片描述
如果我们没有对该 String 对象设置超时时间,则 timestamp 存储的值就是默认值 0, 否则就是该对象过期时间的时间戳, 每次我们获取一个 String 对象的时候, 首先会解析 Value 部分的后四字节, 获取到timestamp 做出判断之后再返回结果。

Hash 结构的存储
Blackwidow 中的 Hash 表由两部分构成,元数据 (meta_key,meta_value) ,和普通数据(data_key,data_value),元数据中存储的主要是 Hash 表的一些信息, 比如说当前 Hash 表的域的数量以及当前 hash 表的版本号和过期时间(用做秒删功能),而普通数据主要就是指的同一个 Hash 表中一一对应的 field 和value,作为具体最后 RocksDB 落盘的 kv 格式,下面是具体的实现方式:

  1. 每个 Hash 表的 meta_key 和 meta_value 的落盘方式:
    在这里插入图片描述
    meta_key 实际上就是 hash 表的 key,而 meta_value 由三个部分构成:4Bytes 的 Hash
    size (用于存储当前hash表的大小) + 4Bytes 的 Version (用于秒删功能) + 4Bytes 的
    Timestamp (用于记录我们给这个Hash 表设置的超时时间的时间戳, 默认为0)。

  2. Hash 表中 data_key 和 data_value 的落盘方式:
    在这里插入图片描述data_key由四个部分构成:4Bytes 的 Key size (用于记录后面追加的key的长度,便与解
    析) + key的内容 + 4Bytes 的 Version + Field 的内容, 而 data_value 就是 Hash 表某个
    field 对应的 value。

如果我们需要查找一个 Hash 表中的某一个 field 对应的 value,我们首先会获取到meta_value解析出其中的 timestamp 判断这个 hash 表是否过期, 如果没有过期, 我们可以拿到其中的version,然后我们使用key,version,和 field 拼出 data_key,进而找到对应的 data_value(如果存在的话)。

List 结构的存储
Blackwidow 中的 List 由两部分构成,元数据(meta_key,meta_value),和普通数据
(data_key,data_value),元数据中存储的主要是 List 链表的一些信息, 比如说当前 List 链表结点的的数量以及当前 List 链表的版本号和过期时间(用做秒删功能),还有当前 List 链表的左右边界(由于 nemo 实现的链表结构被吐槽 lrange 效率低下,所以 Blackwidow 底层用数组来模拟链表,这样 lrange 速度会大大提升,因为结点存储都是有序的),普通数据实际上就是指的list中每一个结点中的数据,作为具体最后 RocksDB 落盘的 kv 格式。pika中性能比较差的情况,使用list时,list的元素不要超过10000个。下面是具体的实现方式:

  1. 每个 List 链表的 meta_key 和 meta_value 的落盘方式:
    在这里插入图片描述
    meta_key实际上就是 List 链表的 key,而 meta_value 由五个部分构成: 8Bytes 的 List size(用于存储当前链表中总共有多少个结点) + 4Bytes 的 Version (用于秒删功能) + 4Bytes 的 Timestamp (用于记录我们给这个List链表设置的超时时间的时间戳, 默认为0)+ 8Bytes 的 Left Index(数组的左边界) + 8Bytes 的Right Index (数组的右边界)。
  2. List 链表中 data_key 和 data_value 的落盘方式:
    在这里插入图片描述
    data_key由四个部分构成:4Bytes 的 Key size (用于记录后面追加的 key 的长度,便与解析) + key 的内容 + 4Bytes 的 Version + 8Bytes 的 Index (这个记录的就是当前结点的在这个 List 链表中的索引), 而data_value 就是 List 链表该 node 中存储的值。

Set 结构的存储
Blackwidow 中的 Set 由两部分构成,元数据 (meta_key,meta_value),和普通数据
(data_key,data_value),元数据中存储的主要是 Set 集合的一些信息, 比如说当前 Set 集合member 的数量以及当前 Set 集合的版本号和过期时间(用做秒删功能),普通数据实际上就是指的 Set 集合中的 member,作为具体最后 RocksDB 落盘的 kv 格式,下面是具体的实现方式:
3. 每个 Set 集合的 meta_key 和 meta_value 的落盘方式:
在这里插入图片描述
meta_key 实际上就是 Set 集合的 key,而 meta_value 由三个部分构成:4Bytes 的 Set size(用于存储当前 Set 集合的大小) + 4Bytes 的 Version(用于秒删功能) + 4Bytes 的Timestamp(用于记录我们给这个 Set 集合设置的超时时间的时间戳, 默认为0)。
4. Set 集合中 data_key 和 data_value 的落盘方式:
在这里插入图片描述
data_key 由四个部分构成:4Bytes 的 Key size(用于记录后面追加的 key 的长度,便与解析) + key 的内容 + 4Bytes 的 Version + member 的内容, 由于 Set 集合只需要存储
member,所以 data_value 实际上就是空串。

ZSet 结构的存储
Blackwidow中 的 ZSet 由两部部分构成,元数据(meta_key,meta_value),和普通数据(data_key,data_value),元数据中存储的主要是 ZSet 集合的一些信息, 比如说当前 ZSet 集合 member 的数量以及当前 ZSet 集合的版本号和过期时间(用做秒删能),而普通数据就是指的 ZSet 中每个 member 以及对应的 score,由于 ZSet 这种数据结构比较特殊,需要按照member 进行排序,也需要按照 score 进行排序, 所以我们对于每一个 ZSet 我们会按照不同的格式存储两份普通数据,在这里我们称为 member to score 和score to member,作为具体最后RocksDB 落盘的 kv 格式,下面是具体的实现方式:
5. 每个 ZSet 集合的 meta_key 和 meta_value 的落盘方式:
在这里插入图片描述
meta_key 实际上就是 ZSet 集合的 key,而 meta_value 由三个部分构成:4Bytes 的ZSet
size(用于存储当前zSet集合的大小) + 4Bytes 的 Version (用于秒删功能) + 4Bytes的
Timestamp (用于记录我们给这个Zset 集合设置的超时时间的时间戳, 默认为0)。
6. 每个 ZSet 集合的 data_key 和 data_value 的落盘方式 (member to score):
在这里插入图片描述
member to socre的 data_key由四个部分构成:4Bytes 的 Key size (用于记录后面追加的key 的长度,便与解析) + key 的内容 + 4Bytes的 Version + member 的内容,data_value中存储的其 member 对应的 score 的值,大小为 8 个字节,由于 RocksDB 默认是按照字典序进行排列的,所以同一个zset中不同的 member 就是按照 member 的字典序来排列的(同一个 ZSet 的 key size,key,以及 version,也就是前缀都是一致的,不同的只有末端的member)。
7. 每个 ZSet 集合的 data_key 和 data_value的落盘方式 (score to member):
在这里插入图片描述
score to member 的 data_key 由五个部分构成:4Bytes 的 Key size(用于记录后面追加的key 的长度,便与解析) + key 的内容 + 4Bytes 的 Version + 8Bytes 的 Score + member的内容, 由于 score 和 member 都已经放在 data_key 中进行存储了所以data_value 就是一个空串,无需存储其他内容了,对于 score to member中 的 data_key 我们自己实现了RocksDB 的 comparetor,同一个 ZSet 中 score to member 的 data_key 会首先按照 score来排序, 在 score 相同的情况下再按照 member 来排序。

三 MyRocks

MyRocks是在Facebook开发的开源软件,目的是将MySQL的功能与RocksDB的实现结合起来。它是基于Oracle MySQL 5.6的。相比 InnoDB,RocksDB 占用更少的存储空间,还具备更高的压缩效率,非常适合大数据量的业务。RocksDB 采用追加的方式记录 DML 操作,将随机写变为顺序写,非常适合用在批量插入和更新频繁的业务场景。

3.1 事务实现

sequence number
RocksDB 中的每一条记录都有一个 sequence number,这个 sequence number 存储在记录的 key 中。
InternalKey:| User key (string) | sequence number (7 bytes) | value type (1byte) |
对于同样的 User key 记录,在 RocksDB 中可能存在多条,但他们的 sequence number不同。sequence number 是实现事务处理的关键,同时也是 MVCC 的基础。

snapshot
snapshot 是 RocksDB 的快照信息,snapshot 实际就是对应一个 sequence number。

假设snapshot 的 sequence number 为 Sa,那么对于此 snapshot 来说,只能看到 sequence number <= Sa 的记录, sequence number > Sa 的记录是不可见的。

RocksDB 的 compact 操作与 snapshot 有紧密联系。以我们熟悉的 InnoDB 为例,RocksDB 的compact 类似于 InnoDB 的 purge 操作, 而 snapshot 类似于 InnoDB 的 read view。 InnoDB做 purge 操作时会根据已有的 read view 来判断哪些 undo log 可以 purge,而 RocksDB 的compact 操作会根据已有 snapshot 信息即全局双向链表来判断哪些记录在 compact 时可以清理。判断的大体原则是,从全局双向链表取出最小的snapshot sequence number Sn。 如果已删除的老记录sequence number <= Sn ,那么这些老记录在 compact 时可以清理掉。

MVCC
有了 snapshot,MVCC 实现起来就很顺利了。记录的 sequence number 天然的提供了记录的多版本信息。 每次查询用户记录时,并不需要加锁。而是根据当前的 sequence number Sn 创建一个 snapshot,查询过程中只取小于或等于 Sn 的最大 sequence number 的记录。查询结束时释放 snapshot。

四 隔离级别

隔离级别也是通过 snapshot 来实现的。在 InnoDB 中,隔离级别为 read-committed 时,事务中每个DML操作都会建立一个 read view,隔离级别为 repeatable-read 时,只在事务开启时建立一次 read view。 RocksDB 同 InnoDB类似,隔离级别为 read-committed 时,事务中每个 DML 操作都会建立一个 snapshot,隔离级别为 repeatable-read 时,只在事务开启时第一个 DML 操作建立一次 snapshot。

五 锁

MyRocks 目前只支持一种锁类型:排他锁(X锁),并且所有的锁信息都保存在内存中。在 RR 隔离界别下只在主键上实现 gap 锁。

你可能感兴趣的:(Redis,数据库,java,开发语言)