事务

当使用TransactionDB或OptimisticTransactionDB时,RocksDB支持事务。事务有一个简单的start /COMMIT/ROLLBACK api,允许应用程序并发地修改数据,同时让RocksDB处理冲突检查。 RocksDB同时支持悲观和乐观并发控制。

注意,当通过WriteBatch编写多个键时,RocksDB默认提供原子性。事务提供了一种方法来确保只有在没有冲突的情况下才会写一批写操作。 与WriteBatch类似,在事务被写入(提交)之前,没有其他线程能够看到事务中的更改。

TransactionDB

当使用TransactionDB时,所有编写的键都由RocksDB在内部锁定,以执行冲突检测。 如果key不能被锁定,操作将返回一个错误。提交事务时,只要能够写入数据库,就保证事务成功。

与OptimisticTransactionDB相比,TransactionDB更适合并发性高的工作负载。 然而,由于锁定开销,使用TransactionDB的成本很小。TransactionDB将对所有写操作执行冲突检查,包括在事务外部执行的写操作。

锁定超时和限制可以在TransactionDBOptions中进行调优。

TransactionDB* txn_db; Status s = TransactionDB::Open(options, path, &txn_db);

  1. Transaction* txn = txn_db->BeginTransaction(write_options, txn_options);
  2. s = txn->Put(“key”, value”);
  3. s = txn->Delete(“key2”);
  4. s = txn->Merge(“key3”, value”);
  5. s = txn->Commit();
  6. delete txn;

默认的写策略被写入。选项有WritePrepared和WriteUnprepared。在这里阅读更多关于他们的信息。( https://github.com/facebook/rocksdb/wiki/WritePrepared-Transactions

OptimisticTransactionDB

乐观事务为不期望多个事务之间存在高争用/干扰的工作负载提供轻量级的乐观并发控制。

乐观事务在准备写操作时不接受任何锁。相反,它们依赖于在提交时执行冲突检测,以验证没有其他作者修改过当前事务正在编写的键。 如果与另一个写发生冲突(或者无法确定),commit将返回一个错误,并且不写键。

乐观并发控制对于许多需要防止偶尔写冲突的工作负载非常有用。但是,对于经常发生写冲突的工作负载来说, 这并不是一个好的解决方案,因为许多事务总是试图更新相同的键。对于这些工作负载,使用TransactionDB可能更合适。 对于有很多非事务性写和很少事务的工作负载,OptimisticTransactionDB可能比TransactionDB性能更好。

  1. DB* db;
  2. OptimisticTransactionDB* txn_db;
  3. Status s = OptimisticTransactionDB::Open(options, path, &txn_db);
  4. db = txn_db->GetBaseDB();
  5. OptimisticTransaction* txn = txn_db->BeginTransaction(write_options, txn_options);
  6. txn->Put(“key”, value”);
  7. txn->Delete(“key2”);
  8. txn->Merge(“key3”, value”);
  9. s = txn->Commit();
  10. delete txn;

从事务中读取

事务还支持轻松读取当前在给定事务中批量处理但尚未提交的key的状态:

  1. db->Put(write_options, a”, old”);
  2. db->Put(write_options, b”, old”);
  3. txn->Put(“a”, new”);
  4. vector<string> values;
  5. vector<Status> results = txn->MultiGet(read_options, {“a”, b”}, &values);
  6. // The value returned for key “a” will be “new” since it was written by this transaction.
  7. // The value returned for key “b” will be “old” since it is unchanged in this transaction.

还可以使用transaction::GetIterator()遍历存在于db和当前事务中的键。

设置一个快照

默认情况下,事务冲突检查验证在此事务中首次编写密钥之后没有其他人编写过key。 这种隔离保证对于许多用例来说已经足够了。但是,您可能希望确保自事务开始以来没有其他人编写过密钥。这可以通过在创建事务之后调用SetSnapshot()来实现。

默认行为:

  1. // Create a txn using either a TransactionDB or OptimisticTransactionDB
  2. txn = txn_db->BeginTransaction(write_options);
  3. // Write to key1 OUTSIDE of the transaction
  4. db->Put(write_options, key1”, value0”);
  5. // Write to key1 IN transaction
  6. s = txn->Put(“key1”, value1”);
  7. s = txn->Commit();
  8. // There is no conflict since the write to key1 outside of the transaction happened before it was written in this transaction.

使用SetSnapshot():

  1. txn = txn_db->BeginTransaction(write_options);
  2. txn->SetSnapshot();
  3. // Write to key1 OUTSIDE of the transaction
  4. db->Put(write_options, key1”, value0”);
  5. // Write to key1 IN transaction
  6. s = txn->Put(“key1”, value1”);
  7. s = txn->Commit();
  8. // Transaction will NOT commit since key1 was written outside of this transaction after SetSnapshot() was called (even though this write
  9. // occurred before this key was written in this transaction).

注意,在前面的示例中,如果这是一个TransactionDB, Put()将会失败。 如果这是一个OptimisticTransactionDB, Commit()将失败。

Repeatable Read 可重复读取

与普通的RocksDB DB读取类似,通过在ReadOptions中设置快照,可以在读取事务时实现可重复读取。

  1. read_options.snapshot = db->GetSnapshot();
  2. s = txn->GetForUpdate(read_options, key1”, &value);
  3. s = txn->GetForUpdate(read_options, key1”, &value);
  4. db->ReleaseSnapshot(read_options.snapshot);

注意,在ReadOptions中设置快照只影响被读取数据的版本。这对能否提交事务没有任何影响。

如果您调用了SetSnapshot(),则可以使用在事务中设置的相同快照进行读取:

  1. read_options.snapshot = txn->GetSnapshot();
  2. Status s = txn->GetForUpdate(read_options, key1”, &value);

防止读写冲突:

GetForUpdate()将确保没有其他写入器修改此事务读取的任何键。

  1. // Start a transaction
  2. txn = txn_db->BeginTransaction(write_options);
  3. // Read key1 in this transaction
  4. Status s = txn->GetForUpdate(read_options, key1”, &value);
  5. // Write to key1 OUTSIDE of the transaction
  6. s = db->Put(write_options, key1”, value0”);

如果这个事务是由TransactionDB创建的,那么Put将超时或阻塞,直到事务提交或中止。 如果这个事务是由OptimisticTransactionDB()创建的,那么Put将成功,但是如果调用txn->Commit(),事务将不会成功。

  1. // Repeat the previous example but just do a Get() instead of a GetForUpdate()
  2. txn = txn_db->BeginTransaction(write_options);
  3. // Read key1 in this transaction
  4. Status s = txn->Get(read_options, key1”, &value);
  5. // Write to key1 OUTSIDE of the transaction
  6. s = db->Put(write_options, key1”, value0”);
  7. // No conflict since transactions only do conflict checking for keys read using GetForUpdate().
  8. s = txn->Commit();

调优/内存使用

在内部,事务需要跟踪最近编写了哪些密钥。现有的内存写缓冲区可用于此目的。当决定在内存中保留多少写缓冲区时,事务仍然遵循现有的max_write_buffer_number选项。 此外,使用事务不会影响刷新或压缩。

切换到使用[乐观的]TransactionDB可能会比以前使用更多的内存。如果您为max_write_buffer_number设置了一个非常大的值,典型的RocksDB实例将永远无法接近这个最大内存限制。 然而,[乐观的]TransactionDB将尝试使用尽可能多的写缓冲区。 但是,可以通过减少max_write_buffer_number或将max_write_buffer_number_to_maintenance设置为小于max_write_buffer_number的值来调优。

OptimisticTransactionDBs:在提交时,乐观事务将使用内存中的写缓冲区来检测冲突。要使此操作成功,所缓冲的数据必须比事务中的更改更早。 如果没有,Commit将失败。为了减少由于缓冲区历史记录不足而导致提交失败的可能性,可以增加max_write_buffer_number_to_maintenance。

TransactionDBs:如果使用SetSnapshot(), Put/Delete/Merge/GetForUpdate操作将首先检查内存缓冲区来进行冲突检测。 如果内存缓冲区中没有足够的数据历史记录,那么将检查SST文件。增加max_write_buffer_number_to_maintenance将减少在冲突检测期间必须读取SST文件的机会。

保存点

除了Rollback(),如果使用保存点,还可以部分回滚事务。

  1. s = txn->Put("A", "a");
  2. txn->SetSavePoint();
  3. s = txn->Put("B", "b");
  4. txn->RollbackToSavePoint()
  5. s = txn->Commit()
  6. // Since RollbackToSavePoint() was called, this transaction will only write key A and not write key B.

引擎盖下面

对事务在幕后如何工作的高级、简洁的概述。

读快照

RocksDB中的每次更新都是通过插入一个标记为单调递增序列号的条目来完成的。 为read_options分配一个seq。快照将被(事务性或非事务性)db用于只读取seq小于该值的值, 即,read snapshots→DBImpl::GetImpl

此外,事务可以调用TransactionBaseImpl::SetSnapshot,它将调用DBImpl::GetSnapshot。它实现了两个目标:

1.返回当前的seq: transactions将使用seq(而不是其写值的seq)检查写-写冲突→TransactionImpl::TryLock→TransactionImpl::ValidateSnapshot→TransactionUtil::CheckKeyForConflicts 2.确保较小的seq值不会被压缩作业等删除(snapshots_.GetAll)。这 样标记的快照必须由被调用方释放(DBImpl::ReleaseSnapshot)

读写冲突检测

可以通过将读写冲突升级为写写冲突来防止读写冲突:通过GetForUpdate(而不是Get)进行读取。

写-写冲突检测:悲观方法

使用锁表在写入时检测写-写冲突。

非事务性更新(put、merge、delete)在事务下在内部运行。所以每次更新都是通过事务→TransactionDBImpl::Put

TransactionLockMgr::TryLock每个列族只有16个锁→size_t num_stripes = 16

Commit通过调用DBImpl:: write→TransactionImpl::Commit将写批处理写入WAL和Memtable

为了支持分布式事务,客户端可以在执行写操作之后调用Prepare。它将值写入WAL而不是MemTable,这允许在机器崩溃时进行恢复→TransactionImpl::Prepare如果调用了Prepare, Commit将提交标记写入WAL并将值写入MemTable。这是通过在向写批处理添加值之前调用MarkWalTerminationPoint()来实现的。

写-写冲突检测:乐观的方法

在提交时使用最新值的seq检测写-写冲突。

每次更新都会将key添加到内存向量→TransactionDBImpl::Put和OptimisticTransactionImpl::TryLock

提交链接OptimisticTransactionImpl::checktransactionforconflict作为写批处理的回调→OptimisticTransactionImpl::Commit, 它将在DBImpl::WriteImpl中通过write->CheckCallback调用

冲突检测逻辑在TransactionUtil::checkkeysforconflict中实现

  • 仅检查与内存中存在的键的冲突,否则将失败。
  • 冲突检测是通过检查每个键(DBImpl::GetLatestSequenceForKey)对用于编写它的seq的最新seq来完成的。