日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 人文社科 > 生活经验 >内容正文

生活经验

Rocksdb的事务(二):完整事务体系的 详细实现

發(fā)布時間:2023/11/27 生活经验 39 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Rocksdb的事务(二):完整事务体系的 详细实现 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

文章目錄

    • 1. 基本事務(wù)操作
      • 1.1 TransactionDB -- Pessimistic
      • 1.2 OptimisticTransactionDB
      • 1.3 Read Uncommitted
      • 1.4 SavePoint 回滾部分事務(wù)操作
      • 1.5 SetSnapshot
      • 1.6 GetForUpdate
      • 1.7 RepeatableRead
    • 2. 實(shí)現(xiàn)
      • 2.1 WBWI(write batch with index) & WB(write batch)
      • 2.2 PessimisticTransaction 實(shí)現(xiàn)
      • 2.3 LockManager 以及 DeadLock detect 實(shí)現(xiàn)
        • 2.3.1 LockManager 加鎖實(shí)現(xiàn)
        • 2.3.2 DeadLock 檢測實(shí)現(xiàn)
      • 2.4 OptimisticTransaction
        • 2.4.1 kValidateSerial occ
        • 2.4.2 kValidateParallel occ
    • 3. 總結(jié)

很久之前簡單介紹了Rocksdb 事務(wù)的基本隔離性的應(yīng)用以及簡單實(shí)現(xiàn)的描述 Rocksdb 事務(wù): 隔離性的實(shí)現(xiàn)(一),其實(shí)里面有一些問題描述的非常模糊,對底層事務(wù)的實(shí)現(xiàn)細(xì)節(jié)講解得也不夠精確且也沒有形成遞進(jìn)體系,所以今年年尾的最后一篇博客希望能夠?qū)?Rocksdb 的事務(wù)體系描述清楚,能夠有效幫助做數(shù)據(jù)庫的同學(xué)了解事務(wù)的基本實(shí)現(xiàn)(基本的隔離級別的實(shí)現(xiàn) rc、ru、si,分布式事務(wù)保證原子性的2PC 以及 事務(wù)周邊的rollback,死鎖檢測/死鎖避免 等都是如何做到的 ),更重要的是能夠看到大規(guī)模工業(yè)應(yīng)用之后的事務(wù)所暴露出來的問題 和 這一些問題對應(yīng)的優(yōu)質(zhì)解決方案,從而能夠讓我們將這一些思想靈活得借鑒到大規(guī)模的工業(yè)分布式環(huán)境之中。

接下來將詳細(xì)展開,以及在LSM-tree 模型下實(shí)現(xiàn)事務(wù)過程中存在的一些問題和對應(yīng)的優(yōu)化策略。

因?yàn)長SM-tree的 append模型,天然支持多版本,而且擁有WriteBatch 可以將多個版本的請求都緩存在內(nèi)存中,從而更有利得解決并發(fā)事務(wù)下的讀寫沖突和寫寫沖突問題。

本文涉及到的源代碼對應(yīng)的 rocksdb 版本是 6.25

1. 基本事務(wù)操作

Rocksdb 的事務(wù)實(shí)現(xiàn) 主要是通過TransactionDB 默認(rèn)是(Pessimistic) 和 OptimisticTransactionDB 來提供事務(wù)操作。

1.1 TransactionDB – Pessimistic

使用 TransactionDB 進(jìn)行事務(wù)操作,默認(rèn)是 PessimisticTransactionDB,每次事務(wù)更新操作都會進(jìn)行加鎖,會去檢測是否和其他事務(wù)有沖突。即 txn1 更新一個key1時會對當(dāng)前key加鎖,事務(wù)txn2 在 txn1 提交前嘗試更新這個key1 則會失敗。

TransactionDB* txn_db;
TransactionDBOptions txndb_opts;
TransactionOptions txn_opts;Options opts;
std::string value;opts.create_if_missing = true;
opts.compression = kNoCompression;Status s = TransactionDB::Open(opts, txndb_opts, "./txn_db", &txn_db);
PrintStatus("TransactionDB::Open", s);// 開啟事務(wù)
Transaction* txn = txn_db->BeginTransaction(WriteOptions(), txn_opts);
assert(txn != nullptr);PrintStatus("txn->Put", txn->Put("key1", "value1"));
/// 其他事務(wù)嘗試更新這個key1,則更新失敗,會有鎖超時問題
PrintStatus("txn_db->Put", txn_db->Put(WriteOptions(), "key1", "value2"));
// 最終的事務(wù)提交
PrintStatus("txn->Commit", txn->Commit());

1.2 OptimisticTransactionDB

在樂觀事務(wù)下,不同事務(wù)之間的沖突檢測不會在每次更新操作時候進(jìn)行檢測,而是在事務(wù)提交的時候進(jìn)行。

/*OptimisticTransactionDB test*/
OptimisticTransactionDB* optimize_txn_db;
OptimisticTransactionOptions opti_txn_opts;
OptimisticTransactionDBOptions opti_txn_db_opts;PrintStatus("OptimisticTransactionDB::Open",OptimisticTransactionDB::Open(opts, "./opti_txn_db", &optimize_txn_db));// 開啟樂觀事務(wù)
Transaction* opti_txn = optimize_txn_db->BeginTransaction(WriteOptions(), opti_txn_opts);
assert(opti_txn != nullptr);// 當(dāng)前事務(wù)更新 key1
PrintStatus("opti_txn->Put", opti_txn->Put("key1", "value1"));
// 外部事務(wù)在當(dāng)前事務(wù)期間更新了 同樣的 key1 ,這里會成功
PrintStatus("optimize_txn_db->Put", optimize_txn_db->Put(WriteOptions(), "key1", "value2"));
// commit 失敗
PrintStatus("opti_txn->Commit", opti_txn->Commit());

悲觀事務(wù) 和 樂觀事務(wù)主要就是沖突檢測的位置不同,所以悲觀事務(wù) 在事務(wù)沖突概率較高的場景下能夠保證提前發(fā)現(xiàn)沖突而更早的觸發(fā)沖突事務(wù)的回滾。在沖突概率不高的情況下,悲觀事務(wù)每一個更新(Put,Delete,Merge,GetForUpdate)都會做沖突檢測,會引入較多的競爭開銷,從而降低性能,所以沖突概率不高的場景可以嘗試樂觀事務(wù)DB。

1.3 Read Uncommitted

這個不是 rocksdb支持的隔離級別,rocksdb 默認(rèn)只支持 RC 和 Repeatable Read

不同的事務(wù)之間只能讀到事務(wù)已經(jīng)提交的數(shù)據(jù),同一個事務(wù)內(nèi)部,能夠讀到未提交的數(shù)據(jù)。

// 事務(wù)寫入key1
PrintStatus("txn->Put", txn->Put("key1", "value1"));
// 讀到key1 的結(jié)果是 value1
PrintStatus("txn->Get", txn->Get(ReadOptions(), "key1", &value));
PrintStatus("txn->Delete", txn->Delete("key1"));
// Notfound
PrintStatus("txn->Get", txn->Get(ReadOptions(), "key1", &value));

因?yàn)槲刺峤坏氖聞?wù)都會在一個WriteBatch中,這樣當(dāng)前事務(wù)內(nèi)部按照操作順序能夠看到當(dāng)前操作之前的所有操作。

1.4 SavePoint 回滾部分事務(wù)操作

Transaction 提供了回滾當(dāng)前事務(wù)部分操作的能力,在事務(wù)中間的某一個位置設(shè)置一個 SavePoint,后面能會滾到這個位置。

// 當(dāng)前事務(wù)寫入
PrintStatus("txn->Put", txn->Put("key1", "value1"));
// 設(shè)置回滾點(diǎn)
txn->SetSavePoint();
// 刪除
PrintStatus("txn->Delete", txn->Delete("key1"));
// 嘗試讀,讀不到,已經(jīng)被刪除了
PrintStatus("txn->Get", txn->Get(ReadOptions(), "key1", &value));
// 回滾
txn->RollbackToSavePoint();
// 此時能夠讀到
PrintStatus("txn->Get", txn->Get(ReadOptions(), "key1", &value));

1.5 SetSnapshot

之前有描述,事務(wù)DB 會對每一個更新的key 進(jìn)行加鎖,保證后續(xù)其他事務(wù)對 相同key的更新是失敗的。如果用戶想要在事務(wù)一開始的時候就標(biāo)識 后續(xù)所有寫入的key 都要進(jìn)行獨(dú)占,那可以通過 開啟事務(wù)之后設(shè)置一個 SetSnapshot 來進(jìn)行后續(xù)當(dāng)前事務(wù) 會獨(dú)占所有標(biāo)識的key,減少一部分的沖突檢測邏輯。

SetSnapshot 之前 我們下面的邏輯是正常的:

Transaction* txn = txn_db->BeginTransaction(WriteOptions(), txn_opts);
assert(txn != nullptr);
// 即使外部的事務(wù)操作成功,因?yàn)槭窃诋?dāng)前事務(wù)更新之前更新的,并沒有沖突
PrintStatus("txn_db->Put", txn_db->Put(WriteOptions(), "key1", "value3"));
PrintStatus("txn->Put", txn->Put("key1", "value1"));PrintStatus("txn->Commit", txn->Commit());

通過 SetSnapshot 標(biāo)識 之后所有的更新都被當(dāng)前事務(wù)獨(dú)占,上面的外部事務(wù)更新就會失敗。

Transaction* txn = txn_db->BeginTransaction(WriteOptions(), txn_opts);
assert(txn != nullptr);
txn->SetSnapshot();
// 即使外部的事務(wù)操作是在當(dāng)前事務(wù)操作之前進(jìn)行更新,這里也會失敗(對setsnapshot 之后所有的操作,當(dāng)前txn都會獨(dú)占)
PrintStatus("txn_db->Put", txn_db->Put(WriteOptions(), "key1", "value3"));
PrintStatus("txn->Put", txn->Put("key1", "value1"));
// 樂觀事務(wù) 會在 Commit 的時候失敗
PrintStatus("txn->Commit", txn->Commit());

悲觀事務(wù) DB 和樂觀 事務(wù)DB的差異是 悲觀事務(wù)DB的 在設(shè)置了snapshot之后的check 會在有操作的時候報(bào)出來;樂觀事務(wù)DB 則會在 commit 的時候報(bào)出來。

1.6 GetForUpdate

有的時候我們 一個事務(wù)內(nèi)部 需要對某一個key 做RMW操作 即 讀改寫,并且保證這個操作是原子的。也就是不能僅僅在寫的時候才獨(dú)占這個key,應(yīng)該在讀的時候就需要獨(dú)占,才能保證后續(xù)當(dāng)前事務(wù)的寫是原子的。 即讀寫沖突的檢測,Rocksdb 目前僅能通過 GetForUpdate 來進(jìn)行讀寫沖突的檢測。

這個時候,就需要 GetForUpdate 接口,保證讀的時候就獨(dú)占這個key

txn = txn_db->BeginTransaction(write_options);// 當(dāng)前事務(wù) 獨(dú)占key1
Status s = txn->GetForUpdate(read_options, “key1”, &value);// 外部事務(wù) 更新 會失敗
s = db->Put(write_options, “key1”, “value0”);

當(dāng)然,對于 悲觀事務(wù)DB 來說 也是提交前就進(jìn)行沖突檢測,樂觀事務(wù)DB則在 Commit的時候進(jìn)行沖突檢測。

1.7 RepeatableRead

可重復(fù)讀 的隔離級別 是一個比較重要的操作,用戶希望能一直讀到一個版本之前的數(shù)據(jù) 而不用擔(dān)心這個版本之后的更新產(chǎn)生對結(jié)果的影響。

TransactionOptions txn_opts;
ReadOptions rops;Transaction* txn = txn_db->BeginTransaction(WriteOptions(), txn_opts);
assert(txn != nullptr);
PrintStatus("txn->put", txn->Put("key1", "value2"));
PrintStatus("txn->Commit", txn->Commit());// 開啟設(shè)置snapshot,標(biāo)識后續(xù)該事務(wù) 對所有自己更新的key都是獨(dú)占的
txn_opts.set_snapshot = true;
txn = txn_db->BeginTransaction(WriteOptions(), txn_opts);
assert(txn != nullptr);// 在其他事務(wù)更新之前設(shè)置一個snapshot
const Snapshot* snapshot = txn->GetSnapshot();
PrintStatus("txn_db->Put", txn_db->Put(WriteOptions(), "key1", "value3"));// 普通的讀,能夠讀到value3
PrintStatus("txn->Get", txn->Get(rops, "key1", &value));// snapshot 讀,讀到的是value2
rops.snapshot = snapshot;
PrintStatus("txn->Get", txn->Get(rops, "key1", &value));

2. 實(shí)現(xiàn)

2.1 WBWI(write batch with index) & WB(write batch)

在看具體事務(wù)實(shí)現(xiàn)之前需要先整體了解一下 WB 以及 WBWI,也就是write batch 和 write batch with index 這兩個為 引擎的更新操作提供的組件。

WB 這里不用說太多,使用基礎(chǔ)DB 的時候經(jīng)常能夠用到,Put 這樣的更新接口被調(diào)用后會組織成一個WriteBatch 數(shù)據(jù)結(jié)構(gòu),將多個更新操作合并成一個請求,從而能夠進(jìn)行原子提交。
事務(wù)DB中,我們使用常見好的事務(wù)DB 直接Put (默認(rèn)是PermisticDB,也會先構(gòu)造WriteBatch,在commit 的時候進(jìn)行原子提交)。
這里的WB 數(shù)據(jù)結(jié)構(gòu)形態(tài)大概如下:

整體是一個string-buf,將一個一個k/v請求拼接進(jìn)去,比較簡單,這里就不過多介紹了。

主要介紹的是 為事務(wù)功能提供的 WBWI,wirte batch with index。其在WriteBatch 基本結(jié)構(gòu)的基礎(chǔ)上構(gòu)造了一個skiplist,用來提供 事務(wù)操作過程中的 read-your-write 以及 savepoint/rollback 等這樣的基本功能。

事務(wù)如果沒有提交,所有的數(shù)據(jù)都還沒有寫入到memtable,都被緩存在WBWI之中。txn->Put 的時候 TryLock 完成之后會去更新WBWI,除了append write-batch 的 string-buffer 之外,會將這個請求在 wb中的offset/cf/key-offset/key-size 這樣的信息形成一個 writeIndexEntry 插入到單獨(dú)維護(hù)的跳表之中。這個時候,后續(xù)的 txn->Get 就能夠有效得讀到之前寫入但是還沒有提交的請求,其體結(jié)構(gòu)如下:

我們的 txn->SetSavePoint 函數(shù)會將 當(dāng)前 WriteBatchWithIndex 中的信息保存到一個 std::stack<SavePoint, autovector<SavePoint>> stack;,當(dāng)前事務(wù)經(jīng)過若干操作之后 后續(xù)的 txn->RollbackToSavePoint() 會進(jìn)行彈棧,并將之前保存的狀態(tài)信息更新到現(xiàn)在的WBWI 之中,從而達(dá)到事務(wù)的回滾的目的。

SetSavePoint 和 RollbackToSavePoint 函數(shù)的邏輯分別在:void WriteBatch::SetSavePoint() ,WriteBatch::RollbackToSavePoint 之中。

因?yàn)閃BWI 是在BeginTransaction 的時候構(gòu)造的,所以每一個事務(wù)會有一個自己獨(dú)立的WBWI,其內(nèi)部的數(shù)據(jù)結(jié)構(gòu)不需要考慮同步問題。

2.2 PessimisticTransaction 實(shí)現(xiàn)

從之前的API 接口測試中,我們能夠知道 PessimisticTransaction 和 OptimisticTransaction 的主要差異是在事務(wù)沖突檢測的時機(jī)上,PessimisticTransaction 在事務(wù)有更新操作 或者 GetForUpdatae 的時候就會嘗試進(jìn)行Key 的獨(dú)占,并進(jìn)行相應(yīng)的沖突檢測。

可看到在 Transaction::Put 的時候會根據(jù)上層配置的transactionDb類型來標(biāo)識應(yīng)該選擇哪一種 TryLock機(jī)制,默認(rèn)是 PessimisticTransaction。TryLock 之后就會更新到 WBWI 之中。

接下來主要看一下 PessimisticTransaction 的 TryLock 是如何實(shí)現(xiàn)key 獨(dú)占 以及 沖突檢測的。

如果用戶配置了 TransactionOptions::skip_concurrency_control=true 的話,這里后面的key 獨(dú)占以及沖突檢測都會直接跳過。
前提是:

  1. 需要應(yīng)用自己保證 不會有key沖突
  2. 需要應(yīng)用保證recovery 的時候所有的回滾和commit 操作 都會新的事務(wù)啟動之前就完成。

這種情況下的性能肯定是最好的, 不過一致性就得自己保證了(應(yīng)用有自己的事務(wù)體系,只用到了Rocksdb的部分事務(wù)能力)。

對輸入的key 的獨(dú)占邏輯如下:

PessimisticTransaction::TryLockPessimisticTransactionDB::TryLockPointLockManager::TryLock

比較老的版本 ,最后key 的加鎖過程入口是 TransactionLockMgr::TryLock,這里重構(gòu)成了 PointLockManager::TryLock,主體邏輯都比較接近,主要是通過LockMgr 來實(shí)現(xiàn),這里后面會詳細(xì)介紹,先關(guān)注主體實(shí)現(xiàn)。

到這里當(dāng)前事務(wù)的 當(dāng)前key 的就加鎖成功了,會獨(dú)占這個key,直到這個事務(wù) Commit 之前其他的事務(wù)都是無法修改這個key,而其他的事務(wù)在這個事務(wù)獨(dú)占成功 但未 提交之前也會進(jìn)入 上面的TryLock 邏輯之中,會嘗試等待這個 key的鎖釋放,直到超時。

回到 PessimisticTransaction::TryLock 邏輯中,前面的加鎖是為了標(biāo)識這個key 后續(xù)不允許其他事務(wù)的修改。但是如果我們在事務(wù)的一開始就設(shè)置了 txn->SetSnapshot,則當(dāng)前事務(wù)后續(xù)所有的更新操作都會默認(rèn)獨(dú)占。可能有這樣的情況,就是在 SetSnapshot 之后 Put之前 外部事務(wù)可能對這個key 進(jìn)行了修改并commit,這樣就不滿足Snapshot的語義了(一個事務(wù)設(shè)置snapshot之后 不允許其他事務(wù)的更新),這里當(dāng)前事務(wù)后續(xù)的提交需要失敗才行,PessimisticTransaction::TryLock 后面的邏輯就是為了做一些沖突檢測。

一個簡略的時序圖場景如下:

因?yàn)?SetSnapshot 操作需要創(chuàng)建Snapshot 并將這個snapshot 插入到全局的雙向鏈表之中,所以會存在一種可能性就是txn1完成snapshot 之前 txn2 寫入并提交了一個新的更新。這個時候,txn1 繼續(xù) Put的話需要語義上失敗。具體過程就是通過 ValidSnapshot 函數(shù) check txn1 開始時 拿到的最新的 seq 和 現(xiàn)在db 中已經(jīng)提交的最新的seq ,如果小的話txn1 就應(yīng)該失敗。

具體檢查的過程就是 從version 系統(tǒng)中 取一個local_version ,直接暴力遍歷這個version 中的 mem/imm,imm-list/sst ,拿到當(dāng)前沖突key 一個最新的seq即可。
邏輯如下:

PessimisticTransaction::ValidateSnapshotTransactionUtil::CheckKeyForConflictsTransactionUtil::CheckKeyDBImpl::GetLatestSequenceForKeysv->mem->Getsv->imm->Getsv->imm->GetFromHistorysv->current->Get

其中哪一步成功了,就直接返回,不需要后續(xù)的讀了,越靠近上層的key-seq 越新。

做完 ValidateSnapshot ,如果當(dāng)前事務(wù)沖突檢測通過了,則繼續(xù)后續(xù)的 WBWI 的更新即可。

到此,整個 PessimisticTransaction 大體 更新邏輯就是這樣的了。
至于 Get 接口,則是直接從 WBWI 中讀取就好了,調(diào)用 WriteBatchWithIndex::GetFromBatchAndDB 就好了;
還有 GetForUpdate 和 更新的邏輯差不多,也是需要先做完 TryLockValidateSnapshot 的檢測才繼續(xù)后續(xù)的讀取。

需要注意的是 我們在txn->SetSnapshot 之后 外部事務(wù)更新 且 后續(xù)的 txn->Get 是沒有問題的,但是 如果讀取使用的是 GetForUpdate ,則會檢測 read-write confict ,GetForUpdate 會失敗;它要求的一致性語義 和 write-write confict 要求的是一樣的。

2.3 LockManager 以及 DeadLock detect 實(shí)現(xiàn)

2.3.1 LockManager 加鎖實(shí)現(xiàn)

在 PessimisticTransaction 的 TryLock 底層,我們會發(fā)現(xiàn)當(dāng)前事務(wù)想要給 key加鎖(保證 commit 之前對key 的獨(dú)占語義 )是通過 LockManager 實(shí)現(xiàn)的,接下來仔細(xì)看一下這個組件的實(shí)現(xiàn)原理,以及其如何進(jìn)行死鎖檢測的。

后文描述的 OptimisticTransaction 事務(wù) 因?yàn)?TryLock 過程不需要檢測鎖沖突,所以不需要用到LockManager.

LockManager 組件結(jié)構(gòu)大概如下:

在整個DB 內(nèi)部維護(hù)了一個大的 LockMaps,它是一個 column family id 到 LockMap的一個 unodered_map。這個DB內(nèi)部的每一個 cf 都有一個自己的LockMap。

// Map of ColumnFamilyId to locked key info
using LockMaps = std::unordered_map<uint32_t, std::shared_ptr<LockMap>>;
LockMaps lock_maps_;

LockMap 內(nèi)部 默認(rèn)會有16個(可以通過 TransactionDBOptions::num_stripes進(jìn)行配置) LockMapStripe,根據(jù)要加鎖的Key 的hash值,會映射到對應(yīng)的LockMapStripe,這里的目的應(yīng)該將輸入的key 打散到不同的stripe中,防止一個stripe 膨脹過大。
每一個 Stripe 內(nèi)部有三個數(shù)據(jù)結(jié)構(gòu),stripe粒度的 mutex 和 conditionvariable,還有一個最重要的 unordered_map,用來保存 不同的key的 LockInfo,lock_info 中會用數(shù)組存儲 操作當(dāng)前key的 transactionId 以及 transaction experation_time,用來進(jìn)行加鎖相關(guān)的判斷。

struct LockInfo {bool exclusive;autovector<TransactionID> txn_ids;// Transaction locks are not valid after this time in usuint64_t expiration_time;...
};

針對一個輸入的key,具體的加鎖過程如下:

  1. 拿到當(dāng)前線程緩存的 lock_maps_cache_,它是一個ThreadLocalPtr(線程性能加速),從中 根據(jù)ColumnFamily ID 找到對應(yīng)的LockMap; 如果 lock_maps_cache_ 沒有, 則從全局的 LockMaps 查找,找到則添加到 lock_maps_cache_
    關(guān)于ThreadLocalPtr的介紹可以查看 rocksdb 讀寫鏈路上的極致優(yōu)化。

  2. 根據(jù) 輸入Key 的Hash,從上一步中拿到的LockMap 中取出 key 被映射到的 LockMapStripe,并對該 Stripe 加鎖(后續(xù)的處理中可能涉及對 stripe 內(nèi)部 unordered_map keys 的更新)同時用 txn->GetID(), txn->GetExpirationTime() 構(gòu)造一個LockInfo

  3. 帶著這個LockInfo 先判斷這個key 所在的 LockMapStripe 的 keys 中是否有這個key,有則說明之前已經(jīng)加過鎖,取出這個 key 在 keys 中 的lockinfo :如果用戶只允許一個key 加鎖 ,即key 的LockInfo 是獨(dú)占鎖,且已有的lockinfo已經(jīng)超時,這里 則替換成 傳入的 LockInfo;如果用戶允許多個事務(wù) 搶占當(dāng)前key 的鎖,會將這個事務(wù)ID 添加到 wait-ids 數(shù)組,等待加鎖;

    如果 LockMapStripe 的 keys 中沒有這個key,則將當(dāng)前key 以及 傳入的LockInfo 添加到keys 對應(yīng)的 unordered_map 之中。

    需要注意的是這里有一個 stripe 允許的最大 加鎖key 的數(shù)量判斷(構(gòu)造 LockManager 的時候 通過TransactionDBOptions::max_num_locks 控制,默認(rèn)是不進(jìn)行限制的),超過這個限制同樣加鎖失敗。

  4. 如果第三步 還是加鎖失敗, 到第四步 會進(jìn)入一個 大的超時循環(huán)中,在這個循環(huán)中會等待加鎖,等待的超時時間是逐 key 對應(yīng)的LockInfo 中的超時時間,如果等待的時間過了超時時間,當(dāng)前事務(wù)就會加鎖失敗返回,每一個 txn 都會進(jìn)入到這個 循環(huán)中進(jìn)行。

2.3.2 DeadLock 檢測實(shí)現(xiàn)

在上面的第四步中,因?yàn)槊恳粋€嘗試加鎖的 txn 在第一次沒有獲取到鎖的時候 都會進(jìn)入到 大循環(huán)中等待獲得鎖,這里并發(fā)事務(wù)場景下可能會出現(xiàn)死鎖問題,所有會有死鎖檢測機(jī)制,實(shí)現(xiàn)邏輯是在 PointLockManager::IncrementWaiters 之中。

這里涉及到 死鎖檢測的幾個參數(shù)如下:

  • TransactionOptions::deadlock_detect 是否開啟死鎖檢測,開啟則會進(jìn)行
  • TransactionOptions::deadlock_detect_depth 因?yàn)榫S護(hù)的是一個 txn 圖,這里會有死鎖檢測的深度設(shè)置,越深肯定消耗的CPU計(jì)算越多

死鎖檢測的核心是想要在一個 有向無環(huán)圖 中檢測 有沒有 wait-circle,即類似如下圖:

上圖中通過尖頭標(biāo)識依賴,A–>C,txn A想要獲得鎖必須等到 txn C 釋放才能獲得。
顯然這個 wait-circle 是不會死鎖的, 只要 txn C 能夠釋放,那么其他的事務(wù)就能繼續(xù)推進(jìn),但是調(diào)整一下C的依賴可能就會死鎖了。

這樣的依賴圖 中出現(xiàn)了環(huán),E–>A, A–>C, C–>E ,那就會死鎖了。

所以,我們的死鎖檢測就是拿著當(dāng)前事務(wù)前面嘗試獲取鎖時 得到的一個 wait-ids 數(shù)組 和已有的wait-circle 來構(gòu)建一個 有向無環(huán)圖,在這個 有向無環(huán)圖中 按照 前面用戶配置的 depth 進(jìn)行 wait-circle 的檢測。

知道了核心目的,這里的代碼邏輯就很簡單了:
死鎖檢測入口是在PointLockManager::IncrementWaiters里面,通過前面對當(dāng)前 txn 構(gòu)造好的 wait-ids 數(shù)組構(gòu)造 wait_txn_map_rev_wait_txn_map_ 兩個 HashMap。

  • rev_wait_txn_map_ 用來保存 活躍事務(wù) 和 該事務(wù)在 有向無環(huán)圖中的節(jié)點(diǎn)個數(shù)(也可以理解為權(quán)重)
  • wait_txn_map_ 用來保存整個活躍事務(wù)視圖,用于 廣度優(yōu)先遍歷。保存的內(nèi)容是 當(dāng)前事務(wù) 和 該事務(wù)有關(guān)聯(lián)的事務(wù)信息TrackedTrxInfo

接下來就是經(jīng)典的廣度優(yōu)先遍歷的實(shí)現(xiàn)了,通過提前resize 好的兩個vector queue_valuesqueue_parents ,resize的大小是 前面說的 deadlock_detect_depth。 queue_values 保存層序,即每一層的所有節(jié)點(diǎn);queue_parents 用來記錄當(dāng)前層的父節(jié)點(diǎn)的下標(biāo),方便后面在發(fā)現(xiàn)死鎖環(huán)之后 進(jìn)行死鎖路徑的回溯。

詳細(xì)的算法實(shí)現(xiàn)這里就不細(xì)講了,廣度優(yōu)先算法而已。

結(jié)束條件是:

  1. deadlock_detect_depth 這個檢測深度下如果沒有發(fā)現(xiàn) 當(dāng)前事務(wù)id 和 已有活躍事務(wù)依賴圖 中不會有相等的情況,則認(rèn)為不會死鎖,返回未發(fā)現(xiàn)死鎖。
  2. 如果發(fā)現(xiàn)有相等的,也就是在依賴圖 下找到了環(huán)的存在,則通過 queue_parents 數(shù)組來回溯當(dāng)前事務(wù)在依賴圖中的依賴路徑,保存到 dlock_buffer_ 中,同時將該事務(wù)信息 從 事務(wù)依賴圖中 踢出 DecrementWaitersImpl,也就是將 當(dāng)前txn 的 wait-ids 逆著走一遍初始化的過程,返回發(fā)現(xiàn)死鎖。

因?yàn)樗梨i檢測到的準(zhǔn)確度與 deadlock_detect_depth 配置有關(guān)系,如果上層的事務(wù)并發(fā)很大,想要有效避免死鎖問題,就需要合理得配置這個選項(xiàng)了,當(dāng)然也需要和性能做一個trade-off,依賴圖很大,深度又很深的話,那一次 Trylock 操作的開銷可就不可忽視了。

這里實(shí)現(xiàn)上有一個細(xì)節(jié),存儲 事務(wù)的依賴圖 rocksdb 并沒有選擇 std 的 unordered_map,而是自己利用 std::arryaautovector(rocksdb 自己的用于小數(shù)據(jù)存儲的vector, 減少擴(kuò)容 以及 添加元素時 產(chǎn)生的內(nèi)存分配的開銷,開始時直接resize了一個大小) 實(shí)現(xiàn)了一個簡易的 HashMap
原因是 std::unordered_map 在 每一次 插入 或者 刪除元素的時候 由于需要維護(hù) iterator 的有效性(紅黑樹需要調(diào)整父子節(jié)點(diǎn)的指針鏈接關(guān)系),需要進(jìn)行內(nèi)存的分配和釋放,這在 TryLock 中 尤其是高并發(fā)下,會造成不必要的開銷。
所以,這里還是選擇用 array + autovector,array 作為hash-bucket,autovector 存儲每一個bucket內(nèi)部的元素即可。

2.4 OptimisticTransaction

接下來看一下 OptimisticTransaction 的實(shí)現(xiàn),相對來說就簡單很多了, 相比于 PessimisticTransaction 的主要差異就是 沖突檢測的時機(jī) 是在 Commit 階段才進(jìn)行。
它在 TryLock 的時候 會將當(dāng)前 要更新的 key的信息添加到 LockTracker 中:

要追蹤的信息主要包括當(dāng)前key的 cf, seq, 是否只讀,是否是獨(dú)占的;其中 seq 是取用戶設(shè)置的snapshot(如果設(shè)置了的話)的 seq,如果沒有設(shè)置,則直接從basedb 中取最新的。

這里 LockTracker 結(jié)構(gòu)大概是這樣的:

為每一個cf 維護(hù)一個 TrackedKeyInfos 的unordered_map,其中保存這個cf 追蹤的key 以及 其 TrackedKeyInfo信息。

接下來 看看 Commit 的時候 樂觀事務(wù)DB 怎么做沖突檢測,按照經(jīng)驗(yàn),看上面記錄的信息,肯定是 seq 是沖突檢測的核心了。
Commit 的代碼中提供了兩種 Occ (optimistic concurrent control)策略: kValidateParallelkValidateSerial ,在2020.6 月份及以前的版本 應(yīng)該是只有一種 occ 策略,也就是 kValidateSerial 的實(shí)現(xiàn)。

先來分別看一下兩種實(shí)現(xiàn)的具體差異,再想想為什么分別會有這兩種策略,希望能為我們在分布式事務(wù)的設(shè)計(jì)中提供一些借鑒思路。

2.4.1 kValidateSerial occ

實(shí)現(xiàn)入口是在 OptimisticTransaction::CommitWithSerialValidate

直接拿到 basedb,調(diào)用 WriteWithCallback 函數(shù),其中 callback 的實(shí)現(xiàn)是 OptimisticTransactionCallback::Callback,也就是 OptimisticTransaction::CheckTransactionForConflicts
WriteWithCallback 會在每次寫入之前對所有要寫入的請求執(zhí)行 callback,WriteImplwriter->CheckCallback(this),檢測是否滿足寫入的要求。

CheckTransactionForConflicts 中做的事情,就是拿著 前面 TryLock 過程中獲取到的 LockTracker 信息 進(jìn)行seq num的比較,和 PessimisticTransaction 事務(wù)中的 ValidSnapshot 做的事情一樣,檢測 tracker 中要提交的key 的 seq 是否比當(dāng)前db 中最新的 已經(jīng)commit 的 seq小,用來確定這個事務(wù)執(zhí)行 中間是否有外部事務(wù) 對當(dāng)前key 的更新(完成 commit的操作)。

	TransactionUtil::CheckKeyForConflictsTransactionUtil::CheckKeyDBImpl::GetLatestSequenceForKeysv->mem->Getsv->imm->Getsv->imm->GetFromHistorysv->current->Get

如果做完沖突檢測,這一批事務(wù)的 tracker key都沒有問題,則可以提交,繼續(xù)后續(xù)的寫入(寫WAL),否則會返回失敗。

occ 策略中的 另一個策略在沖突檢測這里都是差不多的。

2.4.2 kValidateParallel occ

這個策略的入口是 OptimisticTransaction::CommitWithParallelValidate
關(guān)于沖突檢測的內(nèi)部實(shí)現(xiàn)就不過多介紹了,都是一樣的,主要是這個策略如何調(diào)度 沖突檢測的。

在寫入之前 會先對所有的 tracked_locks_ 中的多事務(wù)的 key 按照順序進(jìn)行加鎖,然后統(tǒng)一進(jìn)行 多事務(wù)的沖突檢測,沖突檢測通過之后直接調(diào)度寫入。

這里有一個 issue 來簡單描述了 推出 kValidateParallel occ 策略的原因: kValidateSerial 太慢了。
雖然 optimisticTransaction 是建議 事務(wù)沖突概率較小的場景下才用,但是正常的多cf 寫入的時候有人發(fā)現(xiàn)它比 baseDb和 permisticDB 慢了 5倍,看起來像是很多CPU 沒有被利用起來的樣子。
原因是因?yàn)?舊版本的實(shí)現(xiàn) 也就是 kValidateSerial 策略中:

  1. OptimisticTransactionCallback::AllowWriteBatching 返回是false,也就是在 不會調(diào)度 rocksdb 的寫模型中 的 leader writer 批量寫其他 writer的請求,這樣的話大量的并發(fā)就沒有什么用了, 都是順序?qū)憌al。
  2. OptimisticTransaction 本身就是在 commit 階段進(jìn)行沖突檢測,所以 寫入之前會有一些額外的CPU消耗(拿沖突key 的最新seq的過程 需要從上到下掃LSM-tree,直到拿到當(dāng)前key在basedb 最新提交的seq)

這一些過程導(dǎo)致 在 舊版本的實(shí)現(xiàn)中 性能最差(即使是正常的請求處理),主要影響還是在 利用 Callback 去做沖突檢測的過程為了保證沖突檢測的有效性而關(guān)閉 group commit 功能。

所以,新的版本 主要就是解決這個問題,將callback 去掉,挪到了 write之前,畢竟沖突檢測不論什么時候都得做,而挪到寫鏈路之前做 并不會影響group-commit 的邏輯,沖突檢測通過之后還是正常的 寫邏輯。

相關(guān)的issue和pr 可以參考:
https://github.com/facebook/rocksdb/issues/4402
https://github.com/facebook/rocksdb/pull/6240

occ 策略希望能用一種樂觀的方式處理并發(fā)事務(wù)場景,雖然在高并發(fā)的低沖突事務(wù)處理下優(yōu)勢明顯,但是在正常的事務(wù)處理邏輯中不應(yīng)該有更多的性能損耗, 可能是因?yàn)槭褂脠鼍拜^少的原因,這個性能問題才在18年提出 20年才修復(fù)。

3. 總結(jié)

限于篇幅原因,本來還想繼續(xù)展開 為myrocks 提供的 2PC 和 為 PessimisticTransactionDB 所做的長事務(wù)內(nèi)存優(yōu)化的 WritePreparedTxnDB 以及 WriteUnPreparedTxnDB ,先暫時推后吧。

總的來說,通過Rocksdb 的完整 事務(wù)實(shí)現(xiàn)過程 我們能夠大體了解到 基本隔離級別的實(shí)現(xiàn) 以及 事務(wù)并發(fā)場景時 read-write confict 和 write-write conflict 的有效解決方案。因?yàn)?Rocksdb 是引擎底座,這一些實(shí)現(xiàn)方案 以及 底層的代碼細(xì)節(jié)都是經(jīng)過工業(yè)界長期驗(yàn)證的,值得學(xué)習(xí)參考。


不出意外應(yīng)該是今年的最后一篇博客了,后面幾天會寫一寫年末總結(jié),為這 “多災(zāi)多難” 的一年畫一個句號了。

總結(jié)

以上是生活随笔為你收集整理的Rocksdb的事务(二):完整事务体系的 详细实现的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。