MongoDB中併發控制(MVCC)

在支持行級併發或者文檔級併發的數據庫中,爲了進一步提升系統的併發性,通常都會使用MVCC的機制。
MVCC是一種非鎖機制進行讀操作來提升性能, 我們知道鎖是一種有限的系統資源, 鎖定和解鎖都是需要一定的時間的, 並且如果請求的鎖無法得到的時候, 還需要等待。 如果採用的是互斥鎖, 當該鎖可用的時候, 系統需要通過發送信號來在線程間通信; 另外一種方式是通過自旋鎖的方式, 一直去詢問所需的資源是否準備好, 很浪費CPU資源。
MVCC可以提供基於某個時間點(POV, point-of-view)的快照(snapshot),使得對於事務看來,總是可以提供與事務開始時刻相一致的數據,而不管這個事務執行的時間有多長。所以在不同的事務看來,同一時刻看到的相同行的數據可能是不一樣的,即一個行可能有多個版本。

在WiredTiger裏面, 事務的一致性主要是通過timestamp,每一個timestamp對應一個相應的snapshot,對於每一個事務操作在事務開始的時候, 得到事務的timestamp, 在存儲引擎裏面會有一個snapshot與之對應。該snapshot記錄了該事務在運行過程中的狀態, 並且記錄了事務還沒有提交的事務的[min_snap_id, max_snap_id], 它是從創建snapshot的時候從global_snapshot裏面copy出來的, 記錄了在創建的時候, 還沒有commit的事務列表。在min_snap_id之前的事務是已經提交的事務, 對應的數據是可以讀的, 大於min_snap_id且小於等於max_snap_id是正在運行事務。

隱式事務的框架

在MongoDB4.0中, 不管有沒有顯式地調用一個事務, CRUD操作都會默認地給這些操作創建一個事務, 這是通過WriteUnitOfWork來實現的。WriteUnitOfWork類是一個wrapper類只是設定和取消一些標記, 真正的實現是在WiredTigerRecoveryUnit裏面實現的, 它封裝了server端的事務的使用。
在CRUD下, 都需要通過一個key記過cursor找到btree裏面相應的內存頁上面的KV值, 這裏的cursor需要先通過recover unit得到相應的wiredtiger session類, 此時recover unit會創建一個新的事務並且指定該事務的read_timestamp。

WiredTigerCursor::WiredTigerCursor(const std::string& uri,
                                   uint64_t tableId,
                                   bool forRecordStore,
                                   OperationContext* opCtx) {
    _tableID = tableId;
    _ru = WiredTigerRecoveryUnit::get(opCtx);
    _session = _ru->getSession();
    _cursor = _session->getCursor(uri, tableId, forRecordStore);
}

WiredTigerSession* WiredTigerRecoveryUnit::getSession() {
    if (!_active) {
        _txnOpen();  // 打開一個新的事務
    }
    return _session.get();
}

WiredTigerRecoveryUnit::_txnOpen()主要是用來打開一個新的事務, 並且指定該事務的read_timestamp, 默認情況下, 這個時間點是沒有設定的, 該如何告訴wiredtiger層從那個snapshot裏面讀取哪?
在MongoDB裏面, 可以通過指定ReadSource來實現。ReadSource可以有如下的選擇:

ReadSource 說明
kUnset 這是默認的行爲, 沒有指定timestamp
kNoTimestamp 指定了沒有timestamp的讀取.
kMajorityCommitted 從WiredTigerSnapshotManager::_committedSnapshot
kLastApplied 從WiredTigerSnapshotManager::_localSnapshot讀取
kLastAppliedSnapshot 從WiredTigerSnapshotManager::_localSnapshot讀取
kAllCommittedSnapshot 從一個時間點, 在此之前的所有事務都已經提交了
kProvided 明確地指定了讀取的timestamp, 調用了setTimestampReadSource
void WiredTigerRecoveryUnit::_txnOpen() {
    invariant(!_active);
    _ensureSession();

    // Only start a timer for transaction's lifetime if we're going to log it.
    if (shouldLog(kSlowTransactionSeverity)) {
        _timer.reset(new Timer());
    }
    WT_SESSION* session = _session->getSession();

    switch (_timestampReadSource) {
        case ReadSource::kUnset:
        case ReadSource::kNoTimestamp: {
            WiredTigerBeginTxnBlock txnOpen(session, _ignorePrepared);

            if (_isOplogReader) {
                auto status =
                    txnOpen.setTimestamp(Timestamp(_oplogManager->getOplogReadTimestamp()),
                                         WiredTigerBeginTxnBlock::RoundToOldest::kRound);
                fassert(50771, status);
            }
            txnOpen.done();
            break;
        }
        case ReadSource::kMajorityCommitted: {
            // We reset _majorityCommittedSnapshot to the actual read timestamp used when the
            // transaction was started.
            _majorityCommittedSnapshot =
                _sessionCache->snapshotManager().beginTransactionOnCommittedSnapshot(session);
            break;
        }
        case ReadSource::kLastApplied: {
            if (_sessionCache->snapshotManager().getLocalSnapshot()) {
                _readAtTimestamp = _sessionCache->snapshotManager().beginTransactionOnLocalSnapshot(
                    session, _ignorePrepared);
            } else {
                WiredTigerBeginTxnBlock(session, _ignorePrepared).done();
            }
            break;
        }
        case ReadSource::kAllCommittedSnapshot: {
            if (_readAtTimestamp.isNull()) {
                _readAtTimestamp = _beginTransactionAtAllCommittedTimestamp(session);
                break;
            }
            // Intentionally continue to the next case to read at the _readAtTimestamp.
        }
        case ReadSource::kLastAppliedSnapshot: {
            // Only ever read the last applied timestamp once, and continue reusing it for
            // subsequent transactions.
            if (_readAtTimestamp.isNull()) {
                _readAtTimestamp = _sessionCache->snapshotManager().beginTransactionOnLocalSnapshot(
                    session, _ignorePrepared);
                break;
            }
            // Intentionally continue to the next case to read at the _readAtTimestamp.
        }
        case ReadSource::kProvided: {
            WiredTigerBeginTxnBlock txnOpen(session, _ignorePrepared);
            auto status = txnOpen.setTimestamp(_readAtTimestamp);

            if (!status.isOK() && status.code() == ErrorCodes::BadValue) {
                uasserted(ErrorCodes::SnapshotTooOld,
                          str::stream() << "Read timestamp " << _readAtTimestamp.toString()
                                        << " is older than the oldest available timestamp.");
            }
            uassertStatusOK(status);
            txnOpen.done();
            break;
        }
    }

    LOG(3) << "WT begin_transaction for snapshot id " << _mySnapshotId;
    _active = true;
}

插入修改

在InsertCmd裏面, 會生成一個OperationContext, 並且調用insertDocuments,通過WriteUnitOfWork對象來構造一個事務, 來完成整個過程:

寫入數據

通過函數WiredTigerRecordStore::insertRecords, 將指定的文檔寫入到指定的集合裏面。

   Status WiredTigerRecordStore::_insertRecords(OperationContext* opCtx,
                                           Record* records,
                                           const Timestamp* timestamps,
                                           size_t nRecords) {
  dassert(opCtx->lockState()->isWriteLocked());

  // 生成wt cursor, 在該構造函數會調用_txnOpen, 計算讀取的timestamp;
  WiredTigerCursor curwrap(_uri, _tableId, true, opCtx);
  curwrap.assertInActiveTxn();
  WT_CURSOR* c = curwrap.get();
  invariant(c);

  ...

  for (size_t i = 0; i < nRecords; i++) {
      auto& record = records[i];
      Timestamp ts = timestamps[i];
      if (!ts.isNull()) {
          LOG(4) << "inserting record with timestamp " << ts;
  		// 每一個文檔插入, 設定事務的commit_timestamp
          fassert(39001, opCtx->recoveryUnit()->setTimestamp(ts));
      }
      setKey(c, record.id);
      WiredTigerItem value(record.data.data(), record.data.size());
      c->set_value(c, value.Get());
      int ret = WT_OP_CHECK(c->insert(c));
      if (ret)
          return wtRCToStatus(ret, "WiredTigerRecordStore::insertRecord");
  }

  // 記錄Change並註冊到recover unit的事務裏面
  _changeNumRecords(opCtx, nRecords);
  _increaseDataSize(opCtx, totalLength);

  
  if (_oplogStones) {
      _oplogStones->updateCurrentStoneAfterInsertOnCommit(
          opCtx, totalLength, highestId, nRecords);
  } else {
      cappedDeleteAsNeeded(opCtx, highestId);
  }

  return Status::OK();
}
寫如索引

通過函數IndexCatalogImpl::_indexRecords, 修改索引相關的文檔;索引的數據存放在另外一個集合裏面, 其更新過程如下:

Status IndexCatalogImpl::_indexRecords(OperationContext* opCtx,
                                    IndexCatalogEntry* index,
                                    const std::vector<BsonRecord>& bsonRecords,
                                    int64_t* keysInsertedOut) {
 const MatchExpression* filter = index->getFilterExpression();
 if (!filter)
     return _indexFilteredRecords(opCtx, index, bsonRecords, keysInsertedOut);

 std::vector<BsonRecord> filteredBsonRecords;
 for (auto bsonRecord : bsonRecords) {
     if (filter->matchesBSON(*(bsonRecord.docPtr)))
         filteredBsonRecords.push_back(bsonRecord);
 }

 return _indexFilteredRecords(opCtx, index, filteredBsonRecords, keysInsertedOut);
}

Status IndexCatalogImpl::_indexFilteredRecords(OperationContext* opCtx,
                                            IndexCatalogEntry* index,
                                            const std::vector<BsonRecord>& bsonRecords,
                                            int64_t* keysInsertedOut) {
 InsertDeleteOptions options;
 prepareInsertDeleteOptions(opCtx, index->descriptor(), &options);

 for (auto bsonRecord : bsonRecords) {
     int64_t inserted;
     invariant(bsonRecord.id != RecordId());

     if (!bsonRecord.ts.isNull()) {
         Status status = opCtx->recoveryUnit()->setTimestamp(bsonRecord.ts);
         if (!status.isOK())
             return status;
     }

     Status status = index->accessMethod()->insert(
         opCtx, *bsonRecord.docPtr, bsonRecord.id, options, &inserted);
     if (!status.isOK())
         return status;

     if (keysInsertedOut) {
         *keysInsertedOut += inserted;
     }
 }
 return Status::OK();
}

Status IndexAccessMethod::insert(OperationContext* opCtx,
                              const BSONObj& obj,
                              const RecordId& loc,
                              const InsertDeleteOptions& options,
                              int64_t* numInserted) {
 invariant(numInserted);
 *numInserted = 0;
 BSONObjSet keys = SimpleBSONObjComparator::kInstance.makeBSONObjSet();

 Status ret = Status::OK();
 for (BSONObjSet::const_iterator i = keys.begin(); i != keys.end(); ++i) {
     Status status = _newInterface->insert(opCtx, *i, loc, options.dupsAllowed);

     ...
 }

 if (*numInserted > 1 || isMultikeyFromPaths(multikeyPaths)) {
     _btreeState->setMultikey(opCtx, multikeyPaths);
 }

 return ret;
}

Status WiredTigerIndex::insert(OperationContext* opCtx,
                            const BSONObj& key,
                            const RecordId& id,
                            bool dupsAllowed) {
 dassert(opCtx->lockState()->isWriteLocked());
 invariant(id.isNormal());
 dassert(!hasFieldNames(key));

 Status s = checkKeySize(key);
 if (!s.isOK())
     return s;

 WiredTigerCursor curwrap(_uri, _tableId, false, opCtx);
 curwrap.assertInActiveTxn();
 WT_CURSOR* c = curwrap.get();

 return _insert(opCtx, c, key, id, dupsAllowed);
}

Status WiredTigerIndexStandard::_insert(OperationContext* opCtx,
                                     WT_CURSOR* c,
                                     const BSONObj& keyBson,
                                     const RecordId& id,
                                     bool dupsAllowed) {
 invariant(dupsAllowed);

 TRACE_INDEX << " key: " << keyBson << " id: " << id;

 KeyString key(keyStringVersion(), keyBson, _ordering, id);
 WiredTigerItem keyItem(key.getBuffer(), key.getSize());

 WiredTigerItem valueItem = key.getTypeBits().isAllZeros()
     ? emptyItem
     : WiredTigerItem(key.getTypeBits().getBuffer(), key.getTypeBits().getSize());

 setKey(c, keyItem.Get());
 c->set_value(c, valueItem.Get());
 int ret = WT_OP_CHECK(c->insert(c));

 return Status::OK();
}

寫oplog

OpObserverImpl::onInserts;

WiredTiger 的WT_INSERT

每當有一個insert, 就會在內存頁裏面加入一個WT_INSERT到數組WT_INSERT_HEADER, 它記錄了這個key對應的不同的版本的, 每一個WT_INSERT都有一個WT_UPDATE結構, 以及它對應的page的offset以及size。
在每一個WT_UPDATE裏面都包含了該update的事務id, 以及相關的timestamp。

在這裏插入圖片描述

事務提交

WiredTigerRecoveryUnit::onCommit

修改操作

讀取操作

參考文檔

https://yq.aliyun.com/articles/255163

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章