`
kavy
  • 浏览: 890541 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

RocksDB数据库简介及使用分享

 
阅读更多

目录

 

1 介绍 2

 

1.1 文件介绍: 2

 

2 架构 3

 

3 特性 4

 

3.1 Get,Interator(迭代器)和快照 4

 

3.2 前缀迭代器 5

 

3.3 更新 5

 

3.4 持久化 5

 

3.5 ReadOnly 模式 6

 

3.6 数据库调试日志 6

 

3.7 事务日志 6

 

3.8 Memtable 管道 6

 

3.9 合并 Merge 操作 7

 

3.9.1 合并条件 7

 

4 工具 8

 

5 应用 9

 

5.1 初始化 9

 

5.2 使用 9

 

5.3 参数配置 10

 

5.4 查看数据库数据 10

 

 

 

介绍

RocksDB 项目最开始是在 Facebook 作为一个试验项目开发的高效的数据库软件,可以实现在服务器负载下快速存储(特别是闪存存储)的数据存储的全部潜力。它是一个 C++ 库,可以用于存储 KV,包括任意大小的字节流。它支持原子读写。提供Java 调用api,可以通过Java api 对RocksDB数据库进行操作。

 

RocksDB 具有高度灵活的配置设置,可以调整为在各种生产环境(包括纯内存,闪存,硬盘或 HDFS)上运行。它支持各种压缩算法,并且有生产和调试环境的各种便利工具。        RocksDB 借用了来自开源 leveldb 项目的核心代码,以及来自 Apache HBase 的重要思想。初始代码是从开源 leveldb 1.5 fork 的。它还融入了 facebook 团队在开发 RocksDB 之前的若干代码及想法。

 

文件介绍:

 

 

图 1

 

*.log: 事务日志用于保存数据操作日志,可用于数据恢复

*.sst: 数据持久换文件

MANIFEST:

数据库中的 MANIFEST 文件记录数据库状态。压缩过程会添加新文件并从数据库中删除旧文件,并通过将它们记录在 MANIFEST 文件中使这些操作持久化。

 

CURRENT:记录当前正在使用的MANIFEST文件

LOCK:rocksdb自带的文件锁,防止两个进程来打开数据库。

架构

     RocksDB 是一个嵌入式 kv 存储,key 和 value 是任意字节流。RocksDB 按顺序组织所有数据,常用操作是 Get(key) ,Put(key) ,Delete(key) 和 Scan(key) 。

 

 

 

图 2

 

特性

Get,Interator(迭代器)和快照

Key 和 value 被视为纯字节流。对 key 或 value 的大小没有限制。Get API 允许应用程序从数据库中提取单个 key。MultiGet API 允许应用程序从数据库中检索一堆 key。通过 MultiGet 调用返回的所有 key-value 彼此一致。

 

数据库中的所有数据按照排序顺序进行逻辑排列。应用程序可以定义 key 的排序比较方法。Iterator API 允许应用程序对数据库执行 RangeScan。Iterator 可以寻找指定的 key,然后应用程序可以从该点开始一次扫描一个 key。Iterator API 也可以用于对数据库中的 key 进行反向迭代。创建 Iterator 时,将创建数据库的一致时间点视图。因此,通过 Iterator 返回的所有 key 都来自数据库的一致视图。

 

Snapshot API 允许应用程序创建数据库的时间点视图。Get 和 Iterator API 可用于从指定的快照读取数据。在某种意义上,Snapshot 和 Iterator 都提供了数据库的时间点视图,但它们的实现是不同的。短期扫描最好通过迭代器完成,而长时间运行的扫描最好通过快照完成。迭代器对与数据库的该时间点视图相对应的所有底层文件保持引用计数 - 这些文件在 Iterator 被释放之前不会被删除。另一方面,快照不会防止文件被删除; 但在压缩过程中,压缩程序能够判断快照的存在,它不会删除在任何现有快照中可见的 key。

 

快照不会在数据库重新启动后保持持久化,因此重新加载 RocksDB 库(通过服务器重新启动)会释放所有预先存在的快照。

 

前缀迭代器

大多数 LSM 引擎不能支持高效的 RangeScan API,因为它需要查看每个数据文件。但大多数应用程序不需要对数据库中的 key 范围进行纯随机扫描; 而应用程序通常通过 key 前缀进行扫描。RocksDB 使用这个方法来体现了它的优势。应用程序可以配置 prefix_extractor 以指定 key 前缀。RocksDB 使用它来存储每个 key 前缀的 blooms。指定前缀(通过 ReadOptions)的迭代器将使用这些 bloom 位来避免查找不包含具有指定的 key 前缀的数据文件。

 

 

 

更新

Put API 将单个 key-value 插入数据库。如果 key 已经存在于数据库中,则以前的值将被覆盖。Write API 允许将多个 key-value 原子地插入到数据库中。数据库保证要么单个 Write 调用中的所有 key-value 将被插入数据库,要么它们都不会插入数据库。

 

持久化

RocksDB 有一个事务日志。所有 Put 都存储在称为 memtable 的内存中缓冲区中,并可选择插入到事务日志中。每个 Put 都有一组通过 WriteOptions 设置的标志,它们指定是否将 Put 插入到事务日志中。WriteOptions 还可以指定在 Put 被提交之前,是否向事务日志发出 sync 调用。在内部,RocksDB 使用批量提交机制将多个事务写入到事务日志中,以便它可以使用单个 sync 调用提交多个事务。

 

ReadOnly 模式

数据库可以以只读模式打开,其中数据库保证应用程序不会修改数据库中的任何内容。这导致高得多的读取性能,因为被横穿的代码路径完全避免了锁的开销。

 

数据库调试日志

RocksDB 将详细日志写入名为 LOG* 的文件。这些主要用于调试和分析正在运行的系统。该日志可以被配置为以指定的周期滚动。

 

事务日志

RocksDB 将事务存储到日志文件中以防止系统崩溃。在重新启动时,它会重新处理日志文件中记录的所有事务。日志文件可以配置为存储在与 _sstfile_s 不同的目录中,比如某些场景,你可能会将所有数据文件存储在非持久性快速存储器中,同时,您可以通过将所有事务日志放在较慢但持久的存储上确保不会有数据丢失。

 

Memtable 管道

RocksDB 支持为数据库配置任意数量的 memtable。当 memtable 已满时,它变成不可变的 memtable,后台线程开始将其内容刷新到存储。同时,新的写入继续累积到新分配的 memtable。如果新分配的 memtable 被填充到其限制,它也被转换为不可变的 memtable 并被插入到 flush 管道中。后台线程继续将所有流水线不可变的 memtables 刷新到存储。这种流水线提高了 RocksDB 的写吞吐量,尤其是在慢速存储设备上运行时。

 

合并 Merge 操作

RocksDB 本地支持三种类型的记录:Put 记录,Delete 记录和 Merge 记录。当压缩过程遇到 Merge 记录时,它调用应用程序指定的称为 Merge 的方法。合并可以将多个 Put 和 Merge 记录合并成一个。这个强大的功能允许通常执行读 - 修改 - 写的应用程序完全避免读。它允许应用程序将操作意图记录为合并记录,RocksDB 压缩过程将该意图延迟应用于原始值。此功能在合并运算符中详细描述。

 

合并条件

如果是Level-0层,会先算出当前有多少个没有进行Compact 的文件个数numfiles, 然后根据这个文件的个数进行判断

 

相关参数

 

 

说明

 

level0_file_num_compaction_trigger

 

4

 

当有4个未进行Compact的文件时,达到触发Compact的条件

 

level0_slowdown_writes_trigger

 

20

 

当有20个未进行Compact的文件时,触发RocksDB,减慢写入速度

 

level0_stop_writes_trigger

 

24

 

当有24个未进行Compact的文件时,触发RocksDB停止写入文件,此时会尽快的Compact Level-0层文件

 

表格 1

 

Level-1 层 文件总大小由 max_bytes_for_level_base 参数控制,而 Level-2 层的大小通过: Level_max_bytes[N] = Level_max_bytes[N-1] * max_bytes_for_level_multiplier^(N-1)*max_bytes_for_level_multiplier_additional[N-1] 计算得出

 

参数

 

 

说明

 

max_bytes_for_level_base

 

10485760

 

用于指定Level-1 层总大小,超过这个值满足触发Compact条件

 

max_bytes_for_level_multiplier

 

10

 

每一层最大Bytes 乘法因子

 

max_bytes_for_level_multiplier_addtl[2]

 

1

 

Level-2 层总大小调整参数

 

max_bytes_for_level_multiplier_addtl[3]

 

1

 

Level-3 层总大小调整参数

 

max_bytes_for_level_multiplier_addtl[4]

 

1

 

Level-4 层总大小调整参数

 

max_bytes_for_level_multiplier_addtl[5]

 

1

 

Level-5 层总大小调整参数

 

max_bytes_for_level_multiplier_addtl[6]

 

1

 

Level-6 层总大小调整参数

 

表格 2

 

 

 

对于Level-0层文件,RocksDB总是选择所有的文件进行Compact操作,因为Level-0层的文件之间,可能会有key范围的重叠。 

 

对于Level-N (N>1)层的文件,会先按照文件大小排序(冒泡排序),选出最大的文件,并计算这个文件Key 的起止范围,通过这个范围查找Level-N+1层文件,把选出的Level-N 文件和Level-N+1 文件做为输入,并且在Level-N+1新建一个或多个SST文件作为输出。 

 

可以通过设置max_background_compactions 大于1 来使用并行Compact,不过这个并行Compact 不能作用到Level-0层。

 

工具

sst_dump 工具可以导出 sst 文件中的所有键值对。ldb 工具可以 put,get,scan 数据库的内容。ldb 也可以dump MANIFEST 内容、更改数据库的配置级别数或用于手动压缩数据库。也可以对数据库进行修复(ldb repair –db=数据库路径)

 

 

 

应用

Rocksdb在oJmw中作为嵌入式数据库使用,调用Java接口,需要引入rocksdbjni-版本号.jar包。

 

初始化

RocksDB.loadLibrary();//加载jni

 

RocksDB db = RocksDB.open(options, db_path_not_found)//打开数据库

 

 

 

 

 

使用

单条插入

db.put("hello".getBytes(), "world".getBytes());

 

 

 

批量插入

try (final WriteOptions writeOpt = new WriteOptions()) {

  for (int i = 10; i <= 19; ++i) {

    try (final WriteBatch batch = new WriteBatch()) {

      for (int j = 10; j <= 19; ++j) {

        batch.put(String.format("%dx%d", i, j).getBytes(),

            String.format("%d", i * j).getBytes());

      }

      db.write(writeOpt, batch);

    }

  }

 

查询

value = db.get("world".getBytes());

 

迭代器查询

try (final RocksIterator iterator = db.newIterator()) {

  for (iterator.seekToLast(); iterator.isValid(); iterator.prev()) {

    values.add(iterator.value ());

  }

}

 

 

 

参数配置

接口

 

描述

 

T setCreateIfMissing(boolean flag);

 

 

 

如果数据库不存在则创建

 

T setCreateMissingColumnFamilies(boolean flag);

 

 

 

如果表不存在则创建

 

T setErrorIfExists(boolean errorIfExists);

 

 

 

如果为真则在数据库open 时会抛出异常

 

T setLogger(Logger logger);

 

 

 

 

 

设置日志,可以指定到自己的日志系统

 

T setMaxOpenFiles(int maxOpenFiles);

 

 

 

指定最大文件打开数量

 

T setMaxTotalWalSize(long maxTotalWalSize);

 

 

 

设置事务日志最大值

 

T setUseFsync(boolean useFsync);

 

 

 

设置数据元数据同步下盘

 

setDbPaths(final Collection<DbPath> dbPaths);

 

 

 

设置数据库路径,  [{"/flash_path", 10GB}, {"/hard_drive", 2TB}]

 

越新的数据,越靠前

 

T setDbWriteBufferSize(long dbWriteBufferSize);

 

 

 

设置数据库缓存

 

 

 

查看数据库数据

查看数据库信息两种方式,一种是通过rocksdb提供的ldb工具,一种是利用postman工具通过http请求,来查看

 

 

————————————————

版权声明:本文为CSDN博主「心中的亚雷泽」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/gunri_tianjin/article/details/83444651

 

基本概念

1. LSN (log sequence number)

RocksDB中的每一条记录(KeyValue)都有一个LogSequenceNumber(后面统称lsn),从最初的0开始,每次写入加1。该值为逻辑量,区别于InnoDB的lsn为redo log物理写入字节量。

 

我有几张阿里云幸运券分享给你,用券购买或者升级阿里云相应产品会有特惠惊喜哦!把想要买的产品的幸运券都领走吧!快下手,马上就要抢光了。

这个lsn在RocksDB内部的memtable中是单调递增的,在WriteAheadLog(WAL)中以WriteBatch为单位递增(count(batch.records)为单位)。

WriteBatch是一次RocksDB::Put()的原子操作集合,不同的WriteBatch间是遵循ACID特性(要么完全成功要么完全失败,并且相互隔离),结构如下:

  1.  
    WriteBatch :=
  2.  
    sequence: fixed64
  3.  
    count: fixed32
  4.  
    data: record[count]

从RocksDB外部能看到的LSN是按WriteBatch递增的(LeaderWriter(或LastWriter)最后一次性更新),所以进行snapshot读时,使用的就是此lsn。

注意: 在WAL中每条WriteBatch的lsn并不严格满足以下公式(比如2pc情况下):

lsn(WriteBatch[n]) < lsn(WriteBatch[n+1]),可能相等

2. Snapshot

Snapshot是RocksDB的快照,实际存储的就是一个lsn.

  1.  
    class SnapshotImpl {
  2.  
    public:
  3.  
    // 当前的lsn
  4.  
    SequenceNumber number_;
  5.  
    private:
  6.  
    SnapshotImpl* prev_;
  7.  
    SnapshotImpl* next_;
  8.  
    SnapshotList* list_;
  9.  
    // unix时间戳
  10.  
    int64_t unix_time_;
  11.  
    // 是否属于Transaction(用于写冲突)
  12.  
    bool is_write_conflict_boundary_;
  13.  
    };

查询时如果设置了snapshot为某个lsn, 那么对于此snapshot的读来说,只能看到lsn(key)<=lsn(snapshot)的key,大于该lsn的key是不可见的。

snapshot的创建和删除都需要由一个全局的DoubleLinkList (DBImpl::SnapshotList)管理,天然的根据创建时间(同样也是lsn大小)的关系排序,使用之后需要通过DBImpl::ReleaseSnapshot释放。snapshot还用于在RocksDB事务中实现不同的隔离级别。

3. 隔离级别

为了实现事务下的一致性非锁定读(读可以并发),不同的数据库(引擎)实现了不同的读隔离级别。SQL规范标准中定义了如下四种:

  ReadUncommited ReadCommited RepeatableRead Serializable
Oracle No Yes No Yes
MySQL Yes Yes Yes Yes
RocksDB No Yes Yes No

ReadUncommitted 读取未提交内容,所有事务都可以看到其他未提交事务的执行结果。存在脏读。

ReadCommitted读取已提交内容
,事务只能看见其他已经提交事务所做的改变,多次读取同一个记录可能包含其他事务已提交的更新。

RepeatableRead 可重读,确保事务读取数据时,多次操作会看到同样的数据行(InnoDB通过NextKeyLocking对btree索引加锁解决了幻读)。

Serializable串行化,强制事务之间进行排序,不会互相冲突。

大部分数据库(如MySQL InnoDB、RocksDB),通过MVCC都可以实现上述的在非排它锁锁定情况下的多版本并发读。

RocksDB Transaction

简单的例子:

  1.  
    // 基本配置,事务相关操作需要TransactionDB句柄
  2.  
    Options options;
  3.  
    options.create_if_missing = true;
  4.  
    TransactionDBOptions txn_db_options;
  5.  
    TransactionDB* txn_db;
  6.  
     
  7.  
    // 用支持事务的方式opendb
  8.  
    TransactionDB::Open(options, txn_db_options, kDBPath, &txn_db);
  9.  
     
  10.  
    // 创建一个事务上下文, 类似MySQL的start transaction
  11.  
    Transaction* txn = txn_db->BeginTransaction(write_options);
  12.  
    // 直接写入新数据
  13.  
    txn->Put("abc", "def");
  14.  
    // ForUpdate写,类似MySQL的select ... for update
  15.  
    s = txn->GetForUpdate(read_options, "abc", &value);
  16.  
     
  17.  
    txn->Commit(); // or txn->Rollback();
  18.  
     

RocksDB的一个事物操作,是通过事物内部申请一个WriteBatch实现的,所有commit之前的读都优先读该WriteBatch(保证了同一个事务内可以看到该事务之前的写操作),写都直接写入该事务独有的WriteBatch中,提交时在依次写入WAL和memtable,依赖WriteBatch的原子性和隔离性实现了ACID。

image.png

有些单独写操作也可以通过TransactionDB直接写

  1.  
    txn_db->Put(write_options, "abc", "value");
  2.  
    txn_db->Get(read_options, "abc", &value);

用TransactionDB::Put(),内部会直接生成一个auto transaction,将这个单独的操作封装成一个transaction,并自动commit。所以在TransactionDB中,所有的入口内部都会转化成trasaction(所以显示的transaction是可以马上读取到了外面TransactionDB::Put()的数据,注意这不属于脏读)这个和MySQL的形式是类似的,默认每个SQL都是个auto transaction。但这种transaction是不会触发写冲突检测。

GetForUpdate

类似MySQL的select ... for update,RocksDB提供了GetForUpdate接口。区别于Get接口,GetForUpdate对读记录加独占写锁,保证后续对该记录的写操作是排他的。所以一般GetForUpdate会配合snapshot和SetSnapshotOnNextOperation()进行读,保证多个事务的GetForUpdate都可以成功锁定,而不是一个GetForUpdatech成功其他的失败。尤其是在一些大量基于索引更新的场景上。

事务并发

不同的并发事务之间,如果存在数据冲突,会有如下情况:

  • 事务都是读事务,无论操作的记录间是否有交集,都不会锁定。
  • 事务包含读、写事务:
    • 所有的读事务不会锁定,读到的数据取决于snapshot设置。
    • 写事务之间如果不存在记录交集,不会锁定。
    • 写事务之间如果存在记录交集,此时如果未设置snapshot,则交集部分的记录是可以串行提交的。如果设置了snapshot,则第一个写事务(写锁队列的head)会成功,其他写事务会失败(之前的事务修改了该记录的情况下)。

独占写锁和写冲突

RocksDB事务写锁是基于Key Locking行锁的(实现上锁力度会粗一些),所以在多个Transaction同时更新一条记录,会触发独占写锁定。如果还设置了snapshot的情况下,会触发写冲突分析。每个写操作(Put/Delete/Merge/GetForUpdate)开始之前,会进行写锁定,见TransactionLockMgr代码。如果存在记录有交集,写锁定会锁住一片key保证只有一个事物会独占写。

image.png

内部实现还是比较精炼的,全局有个LockMaps结构,里面按照ColumnFamily级别和num_strips(默认16)级别做了shard进一步降低冲突(此处RocksDB还针对每个LockMap做了ThreadLocal优化)。最底层是一个ColumnFamily下某一个strip的LockMapStripe结构

  1.  
    struct LockMapStripe {
  2.  
    // 当下所有keys共用的os锁
  3.  
    std::shared_ptr<TransactionDBMutex> stripe_mutex;
  4.  
    std::shared_ptr<TransactionDBCondVar> stripe_cv;
  5.  
     
  6.  
    // key -> 记录key, value -> 每个key对应的LockInfo结构
  7.  
    // map中所有的key共享上述os锁,作者这里提到了未来会有更细粒度的锁
  8.  
    // TODO(agiardullo): Explore performance of other data structures.
  9.  
    std::unordered_map<std::string, LockInfo> keys;
  10.  
    };
  11.  
    struct LockInfo {
  12.  
    // 是否是独占锁(也可以是共享锁)
  13.  
    bool exclusive;
  14.  
    // 等待这个key的所有事务链表
  15.  
    autovector<TransactionID> txn_ids;
  16.  
    // 锁超时时间
  17.  
    uint64_t expiration_time;
  18.  
    };

关系图
image.png

针对每一个LockMapStripe里所有的key,有一个LockInfo(包含是否是排它锁,这个key挂的事务ID列表,超时时间)的map,所有落在这个map里的key如果存在并发写的情况,则会等待写锁释放。这里有个粒度问题,两个不相关的key如果落在同一个map里,也会等写锁。不如InnoDB的页锁冲突小,RocksDB作者在注释里提到之后会有更好的方案

加锁代码

  1.  
    Status TransactionImpl::TryLock(ColumnFamilyHandle* column_family,
  2.  
    const Slice& key, bool read_only,
  3.  
    bool exclusive, bool untracked) {
  4.  
     
  5.  
    // tracked_keys_cf记录着当前事务中所有操作的key(涉及所有ColumnFamily)
  6.  
    auto iter = tracked_keys_cf->second.find(key_str);
  7.  
    if (iter == tracked_keys_cf->second.end()) {
  8.  
    // 没找该key说明之前该事务之前一定没有独占锁定这个key
  9.  
    previously_locked = false;
  10.  
    } else {
  11.  
    if (!iter->second.exclusive && exclusive) {
  12.  
    // 如果之前是共享锁,现在申请独占锁,则进行锁升级
  13.  
    lock_upgrade = true;
  14.  
    }
  15.  
    previously_locked = true;
  16.  
    current_seqno = iter->second.seq;
  17.  
    }
  18.  
     
  19.  
    if (!previously_locked || lock_upgrade) {
  20.  
    // 通过全局的LockMgr独占锁定该key(内部使用os锁),如果没有其他事务操作该key(也可
  21.  
    // 能不同的key命中同一个LockMapStrip),则TryLock理解返回并持有该key独占写锁。否则,
  22.  
    // TryLock需要等待其他事务释放该key的独占写锁,或者等待其他事务锁超时
  23.  
    s = txn_db_impl_->TryLock(this, cfh_id, key_str, exclusive);
  24.  
    }
  25.  
     
  26.  
    ......
  27.  
     
  28.  
    // 如果没有设置snapshot方式(可以通过创建事务的TransactionOptions指定snapshot或者
  29.  
    // 调用Transaction的SetSnapshot()方法),则直接获取最新的lsn
  30.  
    if (untracked || snapshot_ == nullptr) {
  31.  
    ......
  32.  
    } else {
  33.  
    // 如果设置了snapshot,需要通过ValidateSnapshot判断是否有其他事务对该key进行了
  34.  
    // 更改(如该事务等待TryLock独占写锁时,其他获得了该锁的事务更新了该key)。具体实现
  35.  
    // 就是是在memtable,immemtable以及sst中取得该key最大的lsn对应的记录(通过
  36.  
    // DBImpl::GetLatestSequenceForKey),看该lsn是否大于当前snapshot的lsn,
  37.  
    // 大于则写冲突。
  38.  
    if (s.ok()) {
  39.  
    s = ValidateSnapshot(column_family, key, current_seqno, &new_seqno);
  40.  
    ........
  41.  
    }
  42.  
    }
  43.  
     
  44.  
    if (s.ok()) {
  45.  
    // 将当前key写入tracked_keys_cf
  46.  
    TrackKey(cfh_id, key_str, new_seqno, read_only, exclusive);
  47.  
    }
  48.  
     
  49.  
    return s;
  50.  
    }

死锁检测/超时

创建事务时 TransactionOptions.deadlock_detect 选项可以支持死锁检测(默认不开启,性能影响较大,尤其是热点记录场景下。依赖timeout机制解决死锁)。如果多个事务之间发生死锁,则当前检测到死锁的事物失败(可以回滚)。死锁检测是通过刚才提到的LockInfo中全局事物ID列表以和当前事务ID进行环检测实现,通过广度优先递归遍历当前事务ID依赖的事务ID,判断其是否指向自己,如果能递归的找到自己的ID则说明有环,发生死锁。deadlock_detect_depth参数可以指定检测的深度,防止过深的依赖。

image.png

Optimistic Transaction

相较于悲观锁,RocksDB也实现了一套乐观锁机制的OptimisticTransaction,接口上和Transaction是一致的。不过在写操作(Put/Delete/Merge/GetForUpdate)时,不会触发独占写锁和写冲突检测,而是在事务commit时("乐观"锁),写入WAL时判断是否存在写冲突,而commit失败。这种方式的好处时,更新操作或者GetForUpdate()时,不用加独占写锁,省去了加锁的代价,乐观的认为没有写冲突,推迟到事务提交时一次性提交所有写入的key进行判断。

MVCC

RocksDB实现的ReadCommited和RepeatableRead隔离级别,类似其他数据库引擎,都使用MVCC机制。例如MySQL的InnoDB,通过undo page实现了行记录的多版本,这样可以在不同的隔离级别下,看到不同时刻的行记录内容。不过undo需要undo页的存储空间以及redo日志的保护(redo写undo),这跟其btree的in-place update有关,而RocksDB依靠其天然的AppendOnly,所有的写操作都是后期merge,自然地就是key的多版本(不同版本可能位于memtable,immemtable,sst),所以RocksDB首先MVCC是很容易的,只需要通过snapshot(lsn)稍加限制即可实现。

例如需要读取比某个lsn小的历史版本,只需要在读取时指定一个带有这个lsn的snapshot,即可读到历史版本。所以,在需要一致性非锁定读读取操作时,默认ReadCommited只需要按照当前系统中最大的lsn读取(这个也是默认DB::Get()的行为),即可读到已经提交的最新记录(提交到memtable后的记录一定是已经commit的记录,未commit之前记录保存在transaction的临时buffer里)。在RepeatableRead下读数据是,需要指定该事务的读上界(即创建事务时的snapshot(lsn)或通过SetSnapshot指定的当时的lsn),已提交的数据一定大于该snapshot(lsn),即可实现可重复读。

  1.  
    txn = txn_db->BeginTransaction(write_options);
  2.  
    // ReadCommited (default)
  3.  
    txn->Get(read_options, "abc", &value);
  4.  
     
  5.  
     
  6.  
    txn = txn_db->BeginTransaction(write_options, txn_options);
  7.  
    txn_options.set_snapshot = true;
  8.  
    // RepeatableRead
  9.  
    read_options.snapshot = txn->GetSnapshot();
  10.  
    s = txn->Get(read_options, "abc", &value);
  11.  
     

可见snapshot对于MVCC有着很重要的意义:

  1. snapshot可以实现不同隔离级别的非锁定读
  2. snapshot可以用于写冲突检测
  3. snapshot由全局的snapshot链表进行管理,在compaction时,会保留该链表中snapshot不被回收

2PC两阶段提交

RocksDB除了实现了基本类型的事务,还实现了2pc(https://github.com/facebook/rocksdb/wiki/Two-Phase-Commit-Implementation。某种程度上看,需求来自于MySQL的MyRocks引擎,binlog和引擎日志(redolog、wal)有一个XA的约束,防止出现写一个日志成功,另一个失败的情况。所以需要引擎日志实现2pc来支持binlog和引擎日志的原子提交。

详细文档可参见 https://github.com/facebook/rocksdb/wiki/Two-Phase-Commit-Implementation

两阶段提交在原有的Transaction基础之上,在写记录和commit之间增加了一个Prepare操作:

  1.  
    BeginTransaction;
  2.  
    Put()
  3.  
    Delete()
  4.  
    .....
  5.  
    Prepare(xid)
  6.  
    Commit(xid) // or Rollback(xid)

2PC实现原理

前面几个步骤和普通的Transaction基本都是一直的,主要是后面Prepare和Commit有所区别。首先,2pc的事务有一个全局的事务表,所有2pc的事务都要有一个name,在设置name的同时,将该事务注册到全局事务表里:

  1.  
    Status TransactionImpl::SetName(const TransactionName& name) {
  2.  
    if (txn_state_ == STARTED) {
  3.  
    ......
  4.  
    // 向事务管理器注册事务
  5.  
    txn_db_impl_->RegisterTransaction(this);
  6.  
    ......
  7.  
    }
  • prepare阶段
  1.  
    // 设置事务状态为开始PREPARE
  2.  
    txn_state_.store(AWAITING_PREPARE);
  3.  
    // PREPARE之后不允许事务超时, 可能会遇到2pc的通病????
  4.  
    expiration_time_ = 0;
  5.  
    WriteOptions write_options = write_options_;
  6.  
    write_options.disableWAL = false;
  7.  
     
  8.  
    // MarkEndPrepare会将当前batch开头和结尾写入PREPARE标记
  9.  
    // 正常的WriteBatch格式一般是:
  10.  
    // Sequence(0);NumRecords(2);Put(a,1);Delete(b);
  11.  
    // MarkEndPrepare之后:
  12.  
    // Sequence(0);NumRecords(4);BeginPrepare();Put(a,1);Delete(b);EndPrepare(transaction_id);
  13.  
    // 对WriteBatch开始和结束分别加入Begin/End,标识是个PREPARE
  14.  
    WriteBatchInternal::MarkEndPrepare(GetWriteBatch()->GetWriteBatch(), name_);
  15.  
    // 将更改之后的WriteBatch写入db,这里只写WAL,不写memtable
  16.  
    s = db_impl_->WriteImpl(write_options, GetWriteBatch()->GetWriteBatch(),
  17.  
    /callback/ nullptr, &log_number_, /log ref/ 0,
  18.  
    / disable_memtable/ true);
  19.  
    if (s.ok()) {
  20.  
    .......
  21.  
    txn_state_.store(PREPARED);
  22.  
    }
  23.  
     

整个过程将修正后的prepared writebatch只是写入WAL日志,并不会更新memtable,这样保证了其他的普通事务和2pc事务是不能访问到该2pc事务的记录(memtable不可见),保证了隔离性。这里有个点需要注意,大部分RocksDB的写操作都是一定写memtable和WAL(可以disable)的,所以全局的LSN就会递增。但prepare步骤是不写入memtable的,所以LSN不会增加,这就解释了文章开头说的WAL中LSN并不一定满足lsn(WriteBatch(n)) < lsn(WriteBatch(n+1))。

  • commit阶段
  1.  
    // 设置事务状态为准备commit
  2.  
    txn_state_.store(AWAITING_COMMIT);
  3.  
     
  4.  
    // 获取临时的一个WriteBatch buffer,区别于prepare之前的操作的WriteBatch
  5.  
    // 所以commit的WriteBatch和prepare的WriteBatch是单独分开的,这也就是说2pc
  6.  
    // 是多个WriteBatch所以需要额外保证原子性。
  7.  
    WriteBatch* working_batch = GetCommitTimeWriteBatch();
  8.  
    // 写入commit标识和事务ID
  9.  
    WriteBatchInternal::MarkCommit(working_batch, name_);
  10.  
     
  11.  
    // WAL终止点(暂没想到更好的叫法),后续写入的数据,WAL会全部忽略
  12.  
    working_batch->MarkWalTerminationPoint();
  13.  
     
  14.  
    // 将包含prepare的全部数据追加到WriteBatch里,这些数据是供memtable写入用的
  15.  
    WriteBatchInternal::Append(working_batch, GetWriteBatch()->GetWriteBatch());
  16.  
     
  17.  
    // 数据写入memtable(包含prepare),并将commit事件写入WAL
  18.  
    s = db_impl_->WriteImpl(write_options_, working_batch, nullptr, nullptr,
  19.  
    log_number_);
  20.  
    if (!s.ok()) {
  21.  
    return s;
  22.  
    }
  23.  
     
  24.  
    // 从全局事务表里删除该事务
  25.  
    txn_db_impl_->UnregisterTransaction(this);

commit阶段主要做两件事:

  1. 将commit标识写入WAL
  2. 将数据写入memtable(让其他事务可以访问到)

整体回顾整个2pc提交的流程,prepare阶段生成BeginPrepare/EndPrepare相关的WAL记录,并写入WAL持久化(这里可以防止crash时,仍旧可以构建出来该事务),但为了保证隔离性,不会写入memtable。commit阶段将Commit的WAL记录写入WAL,并写入memtable,让其他事务可见。这里用了多个WriteBatch,打破了RocksDB默认的单WriteBatch原子性的保证,所以需要在WAL记录中增加额外标识,并在crash时,重建内存2pc事务状态。

2PC Recovery

RocksDB的2pc是跨WriteBatch实现的prepare和commit,所以可能存在中间态,比如prepare之后commit之前crash了。这时候系统启动时要重建所有的正在执行的事务(仅2pc事务,普通事务通过单个WriteBatch已经保证了原子性)。MemtableInserter作为处理WriteBatch中每一条记录,在遇到BeginPrepare/EndPrepare时,会在内存中重建事务的上下文,具体可见MemtableInserter代码本文不赘述。

MyRocks

RocksDB的TransactionDB支持了大部分MySQL对事务的规范,整体接口形式和行为基本一致,有些细节比如online ddl、gap locking的支持、需要binglog开启row模式等

阅读原文

http://click.aliyun.com/m/35296/

分享到:
评论

相关推荐

    cpp-RocksDB是Facebook开源的KV存储基于Google的LevelDB

    MyRocks则是基于RocksDB的MySQL数据库。除了深入内核之外,我们也做了较为详尽的验证,RocksDB在高性能写入、数据压缩上相对于InnoDB有较大的优势。今后,我们将会做更多相关的分享,甚至是内核改进

    simpledb:RocksDB之上的NoSQL嵌入式数据库

    `SimpleDB` 是一个基于 `RocksDB` 的NoSQL嵌入式数据库,它设计用于提供高效、可靠且易于使用的键值存储解决方案。`RocksDB` 是由Facebook开发的一个高性能、可嵌入式的键值对存储系统,它基于Google的LevelDB优化而...

    时序数据库 IoTDB 在 360 的落地实践.pdf

    时序数据库 IoTDB 在 360 的落地实践是讲述 IoTDB 在 360 公司的实践应用及挑战的分享。在这个分享中,作者从业务与选型、时序数据存储形态探索、问题与挑战等方面对 IoTDB 的应用进行了详细的介绍。 首先,从业务...

    2018数据架构选型必读:9月数据库产品技术解析完整版

    RocksDB发布了5.15.10版本,专注于性能的提升和稳定性。SQLServer发布了2019公开预览版,为开发者提供了新的数据库管理和分析能力。PostgreSQL发布了11beta3版本,继续完善其开源数据库的性能与特性。Greenplum也...

    字节跳动分布式数据库实践.pptx

    它采用了RocksDB作为存储引擎,并通过P2P、P3P、P4等分区策略实现数据的分布式存储。RocksDBPSP6P7P8partitionserver是数据库架构中的关键组件,负责数据的分片和调度,以优化读写性能。timestamp server和dashboard...

    State backend Flink-1.13优化及生产实践分享.pdf

    RocksDB提供了更好的性能和可扩展性,但需要更多的运维工作,如调整数据库参数和监控磁盘空间。 在生产实践中,选择合适的State Backend和Checkpoint Storage策略取决于作业的特性和资源限制。对于对延迟敏感的作业...

    tech:编程,数据库,分布式系统

    RocksDB(MyRocks)源码学习—写 RocksDB(MyRocks)原始码学习—读 阿里巴巴MyRocks最佳实践(Percona Live 2017) MySQL MySQL InnoDB日志块结构 MySQL InnoDB事务锁和MVCC MySQL InnoDB日志回滚段崩溃恢复实现...

    实践分享会-1-黄蔚-微众银行 TiDB on ARM 1

    TiDB由多个组件组成,包括用Go语言编写的TiDB Server、PD Server和TiSpark(基于Java和Scala),以及使用Rust语言开发的TiKV Server和RocksDB。此外,Pump用于处理tidb binlog,监控和告警则由Grafana、Prometheus、...

    TiDB 最佳实践.pdf

    TiKV负责分布式事务,MVCC(多版本并发控制),以及与RocksDB等存储引擎的集成。TiDB作为无状态的SQL层,提供了MySQL协议支持、分布式SQL执行计划以及底层存储API,即KV API和Dist SQL API。 **集群管理与调度**: ...

    滴滴NewSQL演进之Fusion实践

    这块是我们所有存储产品的整体架构视图,我们的产品都是在RocksDB引擎层基础之上构建。首先增加了网络层、集群管理层、接入层的工作,构建了我们的NoSQL存储系统Fusion,然后在Fusion的基础之上,我们

    实现在对等网络中的p2p聊天软件

    这可能需要用到分布式数据库技术,如LevelDB或RocksDB,以及版本控制策略来处理并发更新。 最后,**用户体验与界面设计**:聊天软件需要简洁易用的界面,支持文本、语音、视频等多种通信方式。这涉及前端开发,通常...

    kafka-kstream-udemy-scala

    4. **状态存储**:使用Kafka的 RocksDB 插件持久化中间结果,支持容错和故障恢复。 5. **窗口(Windows)**:时间窗口或滑动窗口,用于限制聚合操作的时间范围。 **项目结构** "Kafka-KStream-Udemy-Scala-master...

Global site tag (gtag.js) - Google Analytics