RocksDB源码学习(一):事务

最近在学 RocksDB,本篇博客试图从宏观上理一下 RocksDB 有关事务的部分。内容包含 DB、Transaction、WriteBatch、Sequence Num、Snapshot、Lock 等等。因为刚学,所以如果有理解错误的地方,请谅解。

注:本篇博客除了另行说明的代码截图版本为v5.18.4,其余所有代码框里的代码版本均为 v7.7.3

事务类

首先,和事务有关的主要类关系,如下图所示(画板地址),图只罗列了部分,很不全,画板会更新。

事务类关系

DB 相关

这一部分的类会实现底层的读写等操作,提供 API 供真正的事务去调用,相当于整个事务模块的基石,由接口类 DB 派生而来。

DB类关系

其中,最重要的两个类为 TransactionDB 与 DBImpl,前者实现事务的管理,后者实现 DB 接口类中的各个函数。

TransactionDB

TransactionDB 声明于 include\rocksdb\utilities\transaction_db.h 中,其可以理解为 Transaction 的容器,一个 TransactionDB 内部可以有多个 Transaction,看下这个类的源码,其中就有创建和获取 Transaction。

class TransactionDB : public StackableDB {
 public:
  // Starts a new Transaction.
  //
  // Caller is responsible for deleting the returned transaction when no
  // longer needed.
  //
  // If old_txn is not null, BeginTransaction will reuse this Transaction
  // handle instead of allocating a new one.  This is an optimization to avoid
  // extra allocations when repeatedly creating transactions.
  virtual Transaction* BeginTransaction(
      const WriteOptions& write_options,
      const TransactionOptions& txn_options = TransactionOptions(),
      Transaction* old_txn = nullptr) = 0;

  virtual Transaction* GetTransactionByName(const TransactionName& name) = 0;
  virtual void GetAllPreparedTransactions(std::vector<Transaction*>* trans) = 0;
}
DBImpl

接下来关注 DBImpl,这个类声明于 db\db_impl\db_impl.h 中,可以说是整个事务框架中最重要的一个类,因为它才是真正实现所有读写以及 LSM-Tree 相关操作的实现类,先来看下官方的解释:

While DB is the public interface of RocksDB, and DBImpl is the actual class implementing it. It’s the entrance of the core RocksdB engine. All other DB implementations, e.g. TransactionDB, BlobDB, etc, wrap a DBImpl internally. Other than functions implementing the DB interface, some public functions are there for other internal components to call. For example, TransactionDB directly calls DBImpl::WriteImpl() and BlobDB directly calls DBImpl::GetImpl(). Some other functions are for sub-components to call. For example, ColumnFamilyHandleImpl calls DBImpl::FindObsoleteFiles().

Since it’s a very large class, the definition of the functions is divided in several db_impl_*.cc files, besides db_impl.cc.

也就是说,DB 这个类声明了整个 RocksDB 事务有关的接口,也就是接口类,而 DBImpl 真正实现了这些接口,相当于内部存储引擎的入口类。其他所有的相关实现类,包括 TransactionDB、BlobDB 等等,实际上都是在内部封装了 DBImpl 罢了。所以记住,这个类实现了最底层的读写。

这个类中的成员函数超级多,比如 PUT、Merge、Delete、Write、Get、MultiGet、NewIterator、CreateColumnFamilies、Flush 等等,这里有罗列几个,因为太多了。这些函数的定义大部分都位于同目录下的 db_impl.cc 中,进去就可以看见这些函数的函数体。

注意,我说的是大部分。DBImpl 中的函数太多太复杂了,因此 RocksDB 没有在一个文件中把他们全部定义完,而是又分出了两个派生类:DBImplSecondary 和 DBImplReadOnly,都是 DBImpl 的派生类,位于同级目录下。在 db_impl.cc 中没有定义的函数,均在两个派生类中以 override 的方式实现,定义于 db_impl_secondary.hdb_impl_secondary.ccdb_impl_readonly.hdb_impl_readonly.cc 中,这里不展示代码了。

因此,想要看事务的底层实现,那就着重去看 db_impl_*.cc / .h

Transaction 相关

事务最直接的类就是 Transaction,声明于 include\rocksdb\utilities\transaction.h 中,其中声明了与 snapshot、lock、commit 有关的函数,还有基本的 Get 和 Put。当然,它的底层操作还是要基于 DB 的。

官方对该类的解释如下:

Provides BEGIN/COMMIT/ROLLBACK transactions.

To use transactions, you must first create either an OptimisticTransactionDB or a TransactionDB. See examples/[optimistic_]transaction_example.cc for more information.

To create a transaction, use [Optimistic]TransactionDB::BeginTransaction(). It is up to the caller to synchronize access to this object.

See examples/transaction_example.cc for some simple examples.

可以看出,这个类的创建是基于 OptimisticTransactionDB 或 TransactionDB,所以在前文中说 TransactionDB 就是 Transaction 的容器。该类中的方法由派生类 TransactionBaseImpl 实现,其声明于 utilities\transactions\transaction_base.h 中,方法定义于同级目录下的 transaction_base.cc 中。

Transaction类关系.png

事务分为悲观事务和乐观事务,分别为 PessimisticTransaction 和 OptimisticTransaction。其中,前者认为会通过加锁的方式解决写冲突,后者会通过 version 的方式来解决写冲突。

为了明晰两者的差别,我们先看一下 Transaction::Put 方法:

Status TransactionBaseImpl::Put(ColumnFamilyHandle* column_family,
                                const Slice& key, const Slice& value,
                                const bool assume_tracked) {
  const bool do_validate = !assume_tracked;
  Status s = TryLock(column_family, key, false /* read_only */,
                     true /* exclusive */, do_validate, assume_tracked);

  if (s.ok()) {
    s = GetBatchForWrite()->Put(column_family, key, value);
    if (s.ok()) {
      num_puts_++;
    }
  }

  return s;
}

细节我们先不深究,但是可以看出来,Put 操作,首先要对 key 进行 “加锁”,然后在把写操作写进 WriteBatch 中。Transaction 不会直接把写操作执行,而是写放进 WriteBatch 中,等 Commit 了之后一同写进去,这点和 TinyKV 类似。 至于 WriteBatch 的内容,后面会分析,这里先过掉。

实际上,不管是 PessimisticTransaction 还是 OptimisticTransaction,Put 的时候用到都是上面那个方法体,只不过,那个所谓的 “加锁” 操作变了,也就是 TryLock,直接区分了两种事务的核心。二者均重载了 TryLock,一个以 lock 的方式,一个以 version 的方式。

PessimisticTransaction

其 TryLock 部分源码如下:

// Attempt to lock this key.
// Returns OK if the key has been successfully locked.  Non-ok, otherwise.
// If check_shapshot is true and this transaction has a snapshot set,
// this key will only be locked if there have been no writes to this key since
// the snapshot time.
Status PessimisticTransaction::TryLock(ColumnFamilyHandle* column_family,
                                       const Slice& key, bool read_only,
                                       bool exclusive, const bool do_validate,
                                       const bool assume_tracked) {
  // ...

  // Lock this key if this transactions hasn't already locked it or we require
  // an upgrade.
  if (!previously_locked || lock_upgrade) {
    s = txn_db_impl_->TryLock(this, cfh_id, key_str, exclusive);
  }

  const ColumnFamilyHandle* const cfh =
      column_family ? column_family : db_impl_->DefaultColumnFamily();
  assert(cfh);
  const Comparator* const ucmp = cfh->GetComparator();
  assert(ucmp);
  size_t ts_sz = ucmp->timestamp_size();

  SetSnapshotIfNeeded();

  // Even though we do not care about doing conflict checking for this write,
  // we still need to take a lock to make sure we do not cause a conflict with
  // some other write.  However, we do not need to check if there have been
  // any writes since this transaction's snapshot.
  // TODO(agiardullo): could optimize by supporting shared txn locks in the
  // future.
  SequenceNumber tracked_at_seq =
      status.locked ? status.seq : kMaxSequenceNumber;
  if (!do_validate || (snapshot_ == nullptr &&
                       (0 == ts_sz || kMaxTxnTimestamp == read_timestamp_))) {
    if (assume_tracked && !previously_locked &&
        tracked_locks_->IsPointLockSupported()) {
      s = Status::InvalidArgument(
          "assume_tracked is set but it is not tracked yet");
    }
    // Need to remember the earliest sequence number that we know that this
    // key has not been modified after.  This is useful if this same
    // transaction later tries to lock this key again.
    if (tracked_at_seq == kMaxSequenceNumber) {
      // Since we haven't checked a snapshot, we only know this key has not
      // been modified since after we locked it.
      // Note: when last_seq_same_as_publish_seq_==false this is less than the
      // latest allocated seq but it is ok since i) this is just a heuristic
      // used only as a hint to avoid actual check for conflicts, ii) this would
      // cause a false positive only if the snapthot is taken right after the
      // lock, which would be an unusual sequence.
      tracked_at_seq = db_->GetLatestSequenceNumber();
    }
  } else if (s.ok()) {
    // If a snapshot is set, we need to make sure the key hasn't been modified
    // since the snapshot.  This must be done after we locked the key.
    // If we already have validated an earilier snapshot it must has been
    // reflected in tracked_at_seq and ValidateSnapshot will return OK.
    s = ValidateSnapshot(column_family, key, &tracked_at_seq);

    if (!s.ok()) {
      // Failed to validate key
      // Unlock key we just locked
      if (lock_upgrade) {
        s = txn_db_impl_->TryLock(this, cfh_id, key_str, false /* exclusive */);
        assert(s.ok());
      } else if (!previously_locked) {
        txn_db_impl_->UnLock(this, cfh_id, key.ToString());
      }
    }
  }

  if (s.ok()) {
    // We must track all the locked keys so that we can unlock them later. If
    // the key is already locked, this func will update some stats on the
    // tracked key. It could also update the tracked_at_seq if it is lower
    // than the existing tracked key seq. These stats are necessary for
    // RollbackToSavePoint to determine whether a key can be safely removed
    // from tracked_keys_. Removal can only be done if a key was only locked
    // during the current savepoint.
    //
    // Recall that if assume_tracked is true, we assume that TrackKey has been
    // called previously since the last savepoint, with the same exclusive
    // setting, and at a lower sequence number, so skipping here should be
    // safe.
    if (!assume_tracked) {
      TrackKey(cfh_id, key_str, tracked_at_seq, read_only, exclusive);
    } else {
#ifndef NDEBUG
      if (tracked_locks_->IsPointLockSupported()) {
        PointLockStatus lock_status =
            tracked_locks_->GetPointLockStatus(cfh_id, key_str);
        assert(lock_status.locked);
        assert(lock_status.seq <= tracked_at_seq);
        assert(lock_status.exclusive == exclusive);
      }
#endif
    }
  }

  return s;
}

可以看到,该函数的核心就是通过调用 txn_db_impl_->TryLock 来进行加锁。其中,txn_db_impl 的类型就是 PessimisticTransactionDB,这也反映了 xxxTransaction 调到最后都是 TransactionDB。

PessimisticTransactionDB* txn_db_impl_;

那我们就看一下 PessimisticTransactionDB 的 TryLock 是什么样的,其源码如下:

Status PessimisticTransactionDB::TryLock(PessimisticTransaction* txn,
                                         uint32_t cfh_id,
                                         const std::string& key,
                                         bool exclusive) {
    return lock_manager_->TryLock(txn, cfh_id, key, GetEnv(), exclusive);
}

// 补充
// std::shared_ptr<LockManager> lock_manager_; 

可以看到,最后实际执行加锁的其实是 LockManager 这个类,这个类我们先不管,到这只需要明白 PessimisticTransaction 是通过它来对 key 进行加锁的就行,细节之后再分析。

OptimisticTransaction

不同于 PessimisticTransaction ,OptimisticTransaction 没有 lock 一说,用的是 version 的思想,也就是 MVCC,多版本并发控制。其 TryLock 全部源码如下:

// Record this key so that we can check it for conflicts at commit time.
//
// 'exclusive' is unused for OptimisticTransaction.
Status OptimisticTransaction::TryLock(ColumnFamilyHandle* column_family,
                                      const Slice& key, bool read_only,
                                      bool exclusive, const bool do_validate,
                                      const bool assume_tracked) {
  assert(!assume_tracked);  // not supported
  (void)assume_tracked;
  if (!do_validate) {
    return Status::OK();
  }
  uint32_t cfh_id = GetColumnFamilyID(column_family);

  SetSnapshotIfNeeded();

  SequenceNumber seq;
  if (snapshot_) {
    seq = snapshot_->GetSequenceNumber();
  } else {
    seq = db_->GetLatestSequenceNumber();
  }

  std::string key_str = key.ToString();

  TrackKey(cfh_id, key_str, seq, read_only, exclusive);

  // Always return OK. Confilct checking will happen at commit time.
  return Status::OK();
}

这个 TryLock 就比较简单了,根本就没有锁,它会先生产一个新的 sequenceNum ,然后通过 TrackKey 将其和 key 整合起来。怎么整合的,以及整合后 key 的结构是什么,这些以后都会说,这里先不管这么细。

这个 sequenceNum ,会在 Commit 的时候用来检测冲突,检测操作封装在 CheckTransactionForConflict 中,Commit 会调用这个函数。该函数源码如下:

// Returns OK if it is safe to commit this transaction.  Returns Status::Busy
// if there are read or write conflicts that would prevent us from committing OR
// if we can not determine whether there would be any such conflicts.
//
// Should only be called on writer thread in order to avoid any race conditions
// in detecting write conflicts.
Status OptimisticTransaction::CheckTransactionForConflicts(DB* db) {
  auto db_impl = static_cast_with_check<DBImpl>(db);

  // Since we are on the write thread and do not want to block other writers,
  // we will do a cache-only conflict check.  This can result in TryAgain
  // getting returned if there is not sufficient memtable history to check
  // for conflicts.
  return TransactionUtil::CheckKeysForConflicts(db_impl, *tracked_locks_,
                                                true /* cache_only */);
}

具体怎么检测的,我们先不管。

WriteBatch 相关

如前文所述,事务会将所有的写操作追加进同一个 WriteBatch 中,直到 Commit 时才向 DB 中进行写入。与 WriteBatch 有关的主要为 4 个类,如下:

WriteBatch类关系.png
WriteBatchWithIndex

WriteBatchWithIndex 是 WriteBatch 的辅助结构,额外搞一个 skip-list 来记录每一个操作在 WriteBatch 中的 offset 等信息。其声明于 include/rocksdb/utilities/write_batch_with_index.h 中,官方对它的解释如下:

A WriteBatchWithIndex with a binary searchable index built for all the keys inserted.
In Put(), Merge() Delete(), or SingleDelete(), the same function of the wrapped will be called. At the same time, indexes will be built.
By calling GetWriteBatch(), a user will get the WriteBatch for the data they inserted, which can be used for DB::Write().
A user can call NewIterator() to create an iterator.

从上述可以得到两个信息:

  1. 每一个写操作都会被赋予一个 index;
  2. 通过函数 GetWriteBatch(),可以得到这个事务的 WriteBatch;

这里我们提下第二点。GetWriteBatch() 的定义很简单,就一句话,如下:

WriteBatch* WriteBatchWithIndex::GetWriteBatch() { return &rep->write_batch; }

也即,它会返回 WriteBatchWithIndex 中一个名为 rep 的成员的 write_batch 字段。而 rep 是一个 Rep 类型的结构指针,如下:

class WriteBatchWithIndex {
    // ...
    private:
    	struct Rep;
    	std::unique_ptr<Rep> rep;
}

跟进 Rep 结构,其结构体如下:

struct WriteBatchWithIndex::Rep {
  explicit Rep(const Comparator* index_comparator, size_t reserved_bytes = 0,
               size_t max_bytes = 0, bool _overwrite_key = false,
               size_t protection_bytes_per_key = 0)
      : write_batch(reserved_bytes, max_bytes, protection_bytes_per_key,
                    index_comparator ? index_comparator->timestamp_size() : 0),
        comparator(index_comparator, &write_batch),
        skip_list(comparator, &arena),
        overwrite_key(_overwrite_key),
        last_entry_offset(0),
        last_sub_batch_offset(0),
        sub_batch_cnt(1) {}
  ReadableWriteBatch write_batch;
  WriteBatchEntryComparator comparator;
  Arena arena;
  WriteBatchEntrySkipList skip_list;
  bool overwrite_key;
  size_t last_entry_offset;
  // The starting offset of the last sub-batch. A sub-batch starts right before
  // inserting a key that is a duplicate of a key in the last sub-batch. Zero,
  // the default, means that no duplicate key is detected so far.
  size_t last_sub_batch_offset;
  // Total number of sub-batches in the write batch. Default is 1.
  size_t sub_batch_cnt;

  // Remember current offset of internal write batch, which is used as
  // the starting offset of the next record.
  void SetLastEntryOffset() { last_entry_offset = write_batch.GetDataSize(); }

  // In overwrite mode, find the existing entry for the same key and update it
  // to point to the current entry.
  // Return true if the key is found and updated.
  bool UpdateExistingEntry(ColumnFamilyHandle* column_family, const Slice& key,
                           WriteType type);
  bool UpdateExistingEntryWithCfId(uint32_t column_family_id, const Slice& key,
                                   WriteType type);

  // Add the recent entry to the update.
  // In overwrite mode, if key already exists in the index, update it.
  void AddOrUpdateIndex(ColumnFamilyHandle* column_family, const Slice& key,
                        WriteType type);
  void AddOrUpdateIndex(const Slice& key, WriteType type);

  // Allocate an index entry pointing to the last entry in the write batch and
  // put it to skip list.
  void AddNewEntry(uint32_t column_family_id);

  // Clear all updates buffered in this batch.
  void Clear();
  void ClearIndex();

  // Rebuild index by reading all records from the batch.
  // Returns non-ok status on corruption.
  Status ReBuildIndex();
};

注意到,write_batch 的类型为 ReadableWriteBatch,字面意思就是可读的 WriteBatch,而整个类型里只有一个函数,专门用于通过 offset 来找到 WriteBatch 中某一条写操作,如下:

class ReadableWriteBatch : public WriteBatch {
 public:
  explicit ReadableWriteBatch(size_t reserved_bytes = 0, size_t max_bytes = 0,
                              size_t protection_bytes_per_key = 0,
                              size_t default_cf_ts_sz = 0)
      : WriteBatch(reserved_bytes, max_bytes, protection_bytes_per_key,
                   default_cf_ts_sz) {}
  // Retrieve some information from a write entry in the write batch, given
  // the start offset of the write entry.
  Status GetEntryFromDataOffset(size_t data_offset, WriteType* type, Slice* Key,
                                Slice* value, Slice* blob, Slice* xid) const;
};

因此,重现表述一下第 2 点。WriteBatchWithIndex 可以通过 GetWriteBatch() 获取到事务的 ReadableWriteBatch,进而凭借它来通过 offset 读取到 WriteBatch 中的某一条写操作。

总结一下,在事务没有 Commit 之前,数据还不在 Memtable 中,而是存在 WriteBatch 里,如果有需要,这时候可以通过 WriteBatchWithIndex 来拿到自己刚刚写入的但还没有提交的数据。

事务实现

RocksDB 提供了乐观事务和悲观事务,前者通过 MVCC 进行并发控制,后者采用锁机制,本节就大体介绍一下相关的内容。

乐观事务

sequence number

RocksDB 中的每一条记录都有一个 sequence number, 这个 sequence number 存储在记录的 key 中,整合后的类名为 InternalKey,对应结构名为 ParsedInternalKey。

class InternalKey {
 private:
  std::string rep_;
 public:
  InternalKey() {}  // Leave rep_ as empty to indicate it is invalid
  InternalKey(const Slice& _user_key, SequenceNumber s, ValueType t) {
    AppendInternalKey(&rep_, ParsedInternalKey(_user_key, s, t));
  }
  InternalKey(const Slice& _user_key, SequenceNumber s, ValueType t, Slice ts) {
    AppendInternalKeyWithDifferentTimestamp(
        &rep_, ParsedInternalKey(_user_key, s, t), ts);
  }
}

注意,InternalKey 有两个构造函数,一个含有 ts(time stamp),一个没有。这里需要特别关注的是,ts 是从 v6.6.4 版本才开始有的,在 v5.18.4 以及之前的代码中,根本没有 ts 这个东西。

xsvnaQ.png

在这一小节,我们先不管 ts 是个啥,就按照没有 ts 的 InternalKey 来分析。

那么,rep_ 就是 string 形式的 ParsedInternalKey。

struct ParsedInternalKey {
  Slice user_key;
  SequenceNumber sequence;
  ValueType type;

  ParsedInternalKey()
      : sequence(kMaxSequenceNumber),
        type(kTypeDeletion)  // Make code analyzer happy
  {}                         // Intentionally left uninitialized (for speed)
  // u contains timestamp if user timestamp feature is enabled.
  ParsedInternalKey(const Slice& u, const SequenceNumber& seq, ValueType t)
      : user_key(u), sequence(seq), type(t) {}
  std::string DebugString(bool log_err_key, bool hex) const;

  void clear() {
    user_key.clear();
    sequence = 0;
    type = kTypeDeletion;
  }

  void SetTimestamp(const Slice& ts) {
    assert(ts.size() <= user_key.size());
    const char* addr = user_key.data() + user_key.size() - ts.size();
    memcpy(const_cast<char*>(addr), ts.data(), ts.size());
  }
};

可以看出,InternalKey 的结构如下:

InternalKey结构
  • user_key:key。
  • seq_num:全局递增 sequence number,用于给同一个 key 的不同操作区分先后。
  • value_type:这个和 <key,value> 中的 value 不一样,它实际指的是对这个 key 的操作类型,而不是值类型。比如 Delete、Merge、Rollback 等。

接着,internl_key 会被封装为 memtable_key,与 memtable_value(这个结构我还没找到在哪)一起组成 <key, value> 写进 memtable 中。整个 <key, value> 结构如下:

memtable中的<key,value>

对于同样的 user_key 记录,在 RocksDB 中可能存在多条,但他们的 sequence number 不同。

以我的理解,seq 的主要作用如下:

  • 区分对同一个 key 的所有操作的先后顺序;
  • 用于快照;
  • 无需加锁了;
time stamp (since v6.6.4)

自从 v6.6.4 开始,InternalKey 就加入了 ts 来进行多版本并发控制了。

InternalKey(const Slice& _user_key, SequenceNumber s, ValueType t, Slice ts) {
    AppendInternalKeyWithDifferentTimestamp(
        &rep_, ParsedInternalKey(_user_key, s, t), ts);
}

但是 ParsedInternalKey 的字段并没有变,只是在转化为 string 时,多整合了一个 ts,且是整合进 user_key 中。

void AppendInternalKeyWithDifferentTimestamp(std::string* result,
                                             const ParsedInternalKey& key,
                                             const Slice& ts) {
  assert(key.user_key.size() >= ts.size());
  result->append(key.user_key.data(), key.user_key.size() - ts.size());
  result->append(ts.data(), ts.size());
  PutFixed64(result, PackSequenceAndType(key.sequence, key.type));
}

注意到,虽然加上了 ts,但 ts 使用的是原来 user_key 的末尾空间,也就是 internal_key 的大小并没有变,只是在 user_key 的末尾部分整合上了 ts。同时,在 ParsedInternalKey 中也加入了新的函数,名为 SetTimestamp() ,其功能就是把 ts 整合进 user_key 的末尾成为新的 user_key。

struct ParsedInternalKey {
  // ...
  void SetTimestamp(const Slice& ts) {
    assert(ts.size() <= user_key.size());
    const char* addr = user_key.data() + user_key.size() - ts.size();
    memcpy(const_cast<char*>(addr), ts.data(), ts.size());
  }
}

这样一来,InternalKey 的结构就变成了下面这样:

xy7qbj.png

至于为什么要引入 ts,我现在也没有搞清除。但可以确定的是,它是用来解决并发冲突的,ts 可以充当 version,用来表明每一个记录的产生时间,从而区分出并发事务的先后。可以猜想,seq 做不到的一些并发控制,致使了 ts 的出现。但至于怎么用,这要到后面具体研究读写源码时才能清除,现在我也不知道。

同样的,既然 InternalKey _key 被加上了 ts,那么 memtale_key 中的 internal_key 也要加上对应的 ts。

新的<key, value>

snapshot

snapshot 是 RocksDB 的快照信息,它实际就是对应一个 sequence number 。简单来讲,假设 snapshot 的 sequence number 为 sa,那么对于此 snapshot 来说,只能看到 sequence number <= sa 的记录,其余的均看不见。

snapshot 没有复杂的类继承关系,主要就一条 Snapshot —> SnapshotImpl。

snapshot类关系

其实现类如下:

// Each SnapshotImpl corresponds to a particular sequence number.
class SnapshotImpl : public Snapshot {
 public:
  SequenceNumber number_;  // const after creation
  // It indicates the smallest uncommitted data at the time the snapshot was
  // taken. This is currently used by WritePrepared transactions to limit the
  // scope of queries to IsInSnapshot.
  SequenceNumber min_uncommitted_ = kMinUnCommittedSeq;

  SequenceNumber GetSequenceNumber() const override { return number_; }

  int64_t GetUnixTime() const override { return unix_time_; }

  uint64_t GetTimestamp() const override { return timestamp_; }

 private:
  friend class SnapshotList;

  // SnapshotImpl is kept in a doubly-linked circular list
  SnapshotImpl* prev_;
  SnapshotImpl* next_;

  SnapshotList* list_;                 // just for sanity checks

  int64_t unix_time_;

  uint64_t timestamp_;

  // Will this snapshot be used by a Transaction to do write-conflict checking?
  bool is_write_conflict_boundary_;
};

主要得到的信息如下:

  • snapshot 核心成员为 sequence number,一旦创建就保持不变。
  • 每个 snapshot 创建时都会分配个实际时间和时间戳,分别为 unix_time_ 和 timestmap_。
  • 所有的 snapshot 通过全局双向链表来管理,snapshot 内部维护前后指针。

我们知道,删除记录并不是直接删掉,而是追加一条 Delete 日志,原记录依旧保持。但是,老旧的无用记录不可能一直保存着,所以会在 Merge(又叫 Compact)时清除掉。清除的大致逻辑为,从全局双向链表中取出 sequence number 最小的 snapshot。 如果已删除的老记录 sequence number <= 该snapshot, 那么这些老记录在 Merge 时可以清理掉。

悲观事务

悲观事务通过锁机制来进行并发控制,与其有关的有三大结构体:LockInfo、LockMapStripe、LockMap,三者均位于 utilities/transactions/lock/point/point_lock_manager.cc 中。

LockInfo:

struct LockInfo {
  bool exclusive;
  autovector<TransactionID> txn_ids;

  // Transaction locks are not valid after this time in us
  uint64_t expiration_time;
  
  // ...
};

LockInfo 代表一个锁,分为独占锁和共享锁,由 txn_ids 表示其被哪些事务持有。如果 exclusive 为 true,说明该锁为独占锁,那么仅由 txn_ids[0] 来持有该锁;如果 exclusive 为 false,说明该锁为共享锁,那么由 txn_ids 中的全部事务共同持有。expiration_time 为该锁的超时时间,超过该时间后锁失效。

LockMapStripe:

struct LockMapStripe {
  // ...
  UnorderedMap<std::string, LockInfo> keys;
};

LockMapStripe 维护一个 map,用来指明 key 与 lock 的对应关系,即哪个 key 被哪个 lock 锁住。

LockMap:

struct LockMap {
  // ...
  // Number of sepearate LockMapStripes to create, each with their own Mutex
  const size_t num_stripes_;

  // Count of keys that are currently locked in this column family.
  // (Only maintained if PointLockManager::max_num_locks_ is positive.)
  std::atomic<int64_t> lock_cnt{0};

  std::vector<LockMapStripe*> lock_map_stripes_;

  size_t GetStripe(const std::string& key) const;
};

这个结构看上去就很奇怪,为什么还要费心思维护一个 LockMapStripe* 的 vector,难道一个 LockMapStripe 不够吗?

我是这样猜想的。LockMapStripe 内部就是 hash 映射,那么一旦 key 多起来了,那么就有可能发生 hash 冲突,导致两个不同的 key 映射到了一个 LockInfo 上,很显然是错的。为了减少冲突,RocksDB 采用了多个 LockMapStripe,先把每一个 key 按照自定义的 hash 分到某一个 LockMapStripe 中,然后才在其中 hash 映射到 LockInfo 里,两次不同的 hash,可以大大减少冲突率。

我为什么会这么猜想,来看看 TryLock 的定义:

Status PointLockManager::TryLock(PessimisticTransaction* txn,
                                 ColumnFamilyId column_family_id,
                                 const std::string& key, Env* env,
                                 bool exclusive) {
  // Lookup lock map for this column family id
  std::shared_ptr<LockMap> lock_map_ptr = GetLockMap(column_family_id);
  LockMap* lock_map = lock_map_ptr.get();
  if (lock_map == nullptr) {
    char msg[255];
    snprintf(msg, sizeof(msg), "Column family id not found: %" PRIu32,
             column_family_id);

    return Status::InvalidArgument(msg);
  }

  // Need to lock the mutex for the stripe that this key hashes to
  size_t stripe_num = lock_map->GetStripe(key);
  assert(lock_map->lock_map_stripes_.size() > stripe_num);
  LockMapStripe* stripe = lock_map->lock_map_stripes_.at(stripe_num);

  LockInfo lock_info(txn->GetID(), txn->GetExpirationTime(), exclusive);
  int64_t timeout = txn->GetLockTimeout();

  return AcquireWithTimeout(txn, lock_map, stripe, column_family_id, key, env,
                            timeout, std::move(lock_info));
}

主要是三句:

size_t stripe_num = lock_map->GetStripe(key);
assert(lock_map->lock_map_stripes_.size() > stripe_num);
LockMapStripe* stripe = lock_map->lock_map_stripes_.at(stripe_num);
LockInfo lock_info(txn->GetID(), txn->GetExpirationTime(), exclusive);

然后我们看下 GetStripe() 函数的定义:

size_t LockMap::GetStripe(const std::string& key) const {
  assert(num_stripes_ > 0);
  return FastRange64(GetSliceNPHash64(key), num_stripes_);
}

该函数就是把 key 做了一遍 hash 映射,然后再转换为小于 num_stripes_ 的一个数,也就是致命 key 到底位于哪一个 LockMapStripe 中(实际上是指针,但为了直观就不写 * 了)。

再来看那三句话,首先通过 GetStripe() 找到 key 所处的 LockMapStripe 在 lock_map_stripes_ 中的下标,然后通过 lock_map_stripes_.at() 取出这个 LockMapStripe。这样一看,猜想应该是合理的。

明白了三大结构体之后,继续分析 TryLock() 函数,在获取到 LockMapStripe 之后,执行如下代码:

LockInfo lock_info(txn->GetID(), txn->GetExpirationTime(), exclusive);
int64_t timeout = txn->GetLockTimeout();

return AcquireWithTimeout(txn, lock_map, stripe, column_family_id, key, env,
                          timeout, std::move(lock_info));

它会为当前的事务创建一个 LockInfo,接着进入 AcquireWithTimeout 之中。AcquireWithTimeout 主要是进行 timeout 检查,检查通过之后调用另外的函数进行加锁申请,timeout 检查我们就先跳过。

// Helper function for TryLock().
Status PointLockManager::AcquireWithTimeout(
    PessimisticTransaction* txn, LockMap* lock_map, LockMapStripe* stripe,
    ColumnFamilyId column_family_id, const std::string& key, Env* env,
    int64_t timeout, LockInfo&& lock_info) {
  
  // ... timeout 检查
  // 申请加锁
  result = AcquireLocked(lock_map, stripe, key, env, std::move(lock_info),
                         &expire_time_hint, &wait_ids);
  // ... 根据 result 继续操作
}

AcquireWithTimeout 通过 timeout 检查之后,调用 AcquireLocked 申请加锁。注意一下函数传入的最后一个参数 wait_ids,或者说变量 wait_ids。它用来指明当前事务正在等待哪些事务,可以通过它来表示事务之间的等待顺序,从而检测环路,避免死锁,这个后面会说。

重点来了,进入 AcquireLocked,看看锁是怎么加的。

// Try to lock this key after we have acquired the mutex.
// Sets *expire_time to the expiration time in microseconds
//  or 0 if no expiration.
// REQUIRED:  Stripe mutex must be held.
Status PointLockManager::AcquireLocked(LockMap* lock_map, LockMapStripe* stripe,
                                       const std::string& key, Env* env,
                                       LockInfo&& txn_lock_info,
                                       uint64_t* expire_time,
                                       autovector<TransactionID>* txn_ids) {
  assert(txn_lock_info.txn_ids.size() == 1);

  Status result;
  // Check if this key is already locked
  auto stripe_iter = stripe->keys.find(key);
  if (stripe_iter != stripe->keys.end()) {
    // Lock already held
    LockInfo& lock_info = stripe_iter->second;
    assert(lock_info.txn_ids.size() == 1 || !lock_info.exclusive);

    if (lock_info.exclusive || txn_lock_info.exclusive) {
      if (lock_info.txn_ids.size() == 1 &&
          lock_info.txn_ids[0] == txn_lock_info.txn_ids[0]) {
        // The list contains one txn and we're it, so just take it.
        lock_info.exclusive = txn_lock_info.exclusive;
        lock_info.expiration_time = txn_lock_info.expiration_time;
      } else {
        // Check if it's expired. Skips over txn_lock_info.txn_ids[0] in case
        // it's there for a shared lock with multiple holders which was not
        // caught in the first case.
        if (IsLockExpired(txn_lock_info.txn_ids[0], lock_info, env,
                          expire_time)) {
          // lock is expired, can steal it
          lock_info.txn_ids = txn_lock_info.txn_ids;
          lock_info.exclusive = txn_lock_info.exclusive;
          lock_info.expiration_time = txn_lock_info.expiration_time;
          // lock_cnt does not change
        } else {
          result = Status::TimedOut(Status::SubCode::kLockTimeout);
          *txn_ids = lock_info.txn_ids;
        }
      }
    } else {
      // We are requesting shared access to a shared lock, so just grant it.
      lock_info.txn_ids.push_back(txn_lock_info.txn_ids[0]);
      // Using std::max means that expiration time never goes down even when
      // a transaction is removed from the list. The correct solution would be
      // to track expiry for every transaction, but this would also work for
      // now.
      lock_info.expiration_time =
          std::max(lock_info.expiration_time, txn_lock_info.expiration_time);
    }
  } else {  // Lock not held.
    // Check lock limit
    if (max_num_locks_ > 0 &&
        lock_map->lock_cnt.load(std::memory_order_acquire) >= max_num_locks_) {
      result = Status::Busy(Status::SubCode::kLockLimit);
    } else {
      // acquire lock
      stripe->keys.emplace(key, std::move(txn_lock_info));

      // Maintain lock count if there is a limit on the number of locks
      if (max_num_locks_) {
        lock_map->lock_cnt++;
      }
    }
  }

  return result;
}
  1. 通过 LockMapStripe 来判断这个 key 是否已经被加锁了,如果是,进入2,如果否,进入 6。
  2. key 已经被加锁,那么判断 key 的锁是否为独占锁,或者当前事务的锁是否为独占锁,两者任意一个通过,进入3,否则进入 5。
  3. 判断持有该 key 的锁的事务是否为当前事务,如果是,就按照事务的锁更新一些字段然后返回即可,如果不是,进入 4。
  4. 判断该 key 的锁是否超时,如果是,那么该锁会被当前事务的锁替代,该 key 成功由当前事务锁住,然后返回;如果不是,给个 kLockTimeout 错误状态,然后把 txn_ids(wait_ids)赋值为持有该 key 锁的所有事务id,表示当前事务要等他们释放锁,返回。
  5. 进入这一步说明 key 的锁和当前事务的锁其中至少有一个是共享锁。那么就直接把当前事务加入进 key 锁的事务 vector 中,一起共享这个锁,然后重算锁的超时时间,取两者之间的最大值,返回。
  6. 进入这一步说明 key 并没有被锁住。查看 LockMap 中的锁数量是否超过了限制,如果是,给个 kLockLimit 错误状态然后返回即可;如果否,就在 kLockTimeout 加一条映射,表示该 key 被当前事务锁住了,之后吧 LockMap 中的锁数量加一,返回。

至此,加锁申请完毕,对锁的大致流程也有了一定的了解。

实际上,上述流程是以 CF 为单位的,一个 CF 拥有一个 LockMap。

class PointLockManager : public LockManager {
  // ...
 private:
  // ...
  // Map of ColumnFamilyId to locked key info
  using LockMaps = UnorderedMap<uint32_t, std::shared_ptr<LockMap>>;
  LockMaps lock_maps_;
  // ...
}

接下来关注 wait_ids。当 AcquireWithTimeout 调用完 AcquireLocked 之后,在后续执行了如下一段代码:

// We are dependent on a transaction to finish, so perform deadlock
// detection.
if (wait_ids.size() != 0) {
    if (txn->IsDeadlockDetect()) {
        if (IncrementWaiters(txn, wait_ids, key, column_family_id,
                             lock_info.exclusive, env)) {
            result = Status::Busy(Status::SubCode::kDeadlock);
            stripe->stripe_mutex->UnLock();
            return result;
        }
    }
    txn->SetWaitingTxn(wait_ids, column_family_id, &key);
}

可以看到,函数通过 wait_ids 维护的事务等待队列来进行死锁检测。死锁检测的核心函数即为 IncrementWaiters(),其定义我们暂且先不看,后续会专门总结一篇博客分析 RocksDB 检测死锁的实现。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
根据Valgrind提供的信息,可以得出以下分析: 这段Valgrind信息表示在程序运行结束时,有24字节的内存块是明确丢失的。这是在294条记录中的第68条记录。 这个内存块的分配是通过`operator new`函数进行的,具体是在`vg_replace_malloc.c`文件的`operator new(unsigned long, std::nothrow_t const&)`函数中进行的。这个函数用于分配内存,并且使用了`std::nothrow_t`参数,表示在分配失败时不抛出异常。 这个内存块的丢失发生在`libstdc++.so.6.0.19`库文件中的`__cxa_thread_atexit`函数中。这个函数是C++标准库中的一个线程退出钩子函数,用于在线程退出时执行清理操作。 进一步跟踪,这个内存块的丢失是在`librocksdb.so.6.20.3`库文件中的`rocksdb::InstrumentedMutex::Lock()`函数中发生的。这个函数是RocksDB数据库引擎的一个锁操作函数,用于获取互斥锁。 在调用堆栈中,可以看到这个内存块丢失是在RocksDB数据库引擎的后台合并线程(Background Compaction)中发生的。具体是在`rocksdb::DBImpl::BackgroundCallCompaction()`和`rocksdb::DBImpl::BGWorkCompaction()`函数中进行的合并操作。 最后,从调用堆栈中可以看到,这个内存块的丢失是在后台线程中发生的。这是在`librocksdb.so.6.20.3`库文件中的`rocksdb::ThreadPoolImpl::Impl::BGThread()`和`rocksdb::ThreadPoolImpl::Impl::BGThreadWrapper()`函数中执行的。 综上所述,根据Valgrind的信息分析,这段代码中存在一个明确的内存泄漏问题,24字节的内存块在后台合并线程中丢失。需要进一步检查代码,确保在合适的时机释放这些内存块,以避免资源泄漏和潜在的问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值