Rocksdb 事务(一): 隔离性的实现
文章目錄
- 前言
- 1. 隔離性
- 2. Rocksdb實(shí)現(xiàn)的隔離級(jí)別
- 2.1 常見的四種隔離級(jí)別
- 2.2 Rocksdb 支持的隔離級(jí)別及基本實(shí)現(xiàn)
- 2.2.1 ReadComitted 隔離級(jí)別的測(cè)試
- 2.2.2 ReadCommitted的實(shí)現(xiàn)
- 2.2.3 RepeatableRead的實(shí)現(xiàn)
- 2.2.4 事務(wù)并發(fā)處理
- 3. 一些總結(jié)
前言
Rocksdb 作為單機(jī)存儲(chǔ)引擎,已經(jīng)非常成熟得應(yīng)用在了許多分布式存儲(chǔ)(CEPH, TiKV),以及十分通用的數(shù)據(jù)庫(kù)之上(mysql, mongodb, Drango等),所以Rocksdb本身需要能夠?qū)崿F(xiàn)ACID屬性,尤其是其中的不同的隔離級(jí)別才能夠作為一個(gè)公共的存儲(chǔ)組件。本節(jié),結(jié)合rocksdb6.4.6代碼以及官網(wǎng)wiki來梳理一下rocksdb的事務(wù)管理以及隔離性的實(shí)現(xiàn)。
1. 隔離性
ACID中的隔離性意味著 同時(shí)執(zhí)行的事務(wù)之間是互不影響的。這個(gè)時(shí)候,在一些同時(shí)執(zhí)行事務(wù)的場(chǎng)景下,就需要有針對(duì)事務(wù)的隔離級(jí)別,來滿足客戶端針對(duì)存儲(chǔ)系統(tǒng)的要求。
圖1.1 兩個(gè)客戶之間的競(jìng)爭(zhēng)狀態(tài)同時(shí)遞增計(jì)數(shù)器
如上圖1.1,user1和user2對(duì)數(shù)據(jù)庫(kù)的訪問
- user1先從數(shù)據(jù)庫(kù)中g(shù)et,得到了42。完成get事務(wù)之后拿著get的結(jié)果+1,將43set到數(shù)據(jù)庫(kù)中
- user1下發(fā)set的同時(shí)user2從數(shù)據(jù)庫(kù)中g(shù)et,同樣得到了42,也進(jìn)行42+1 的操作
- 兩者的事務(wù)都是各自隔離的,且是串行執(zhí)行互不影響(user2的get并無法同時(shí)訪問user1 set的結(jié)果),保證了結(jié)果對(duì)用戶的正確性
圖1.2 違反了隔離性:一個(gè)事務(wù)讀取了另一個(gè)事務(wù)執(zhí)行的結(jié)果
如上圖中,user2將user1的insert過程中的 hello 作為了自己的輸入,即一個(gè)事務(wù)能夠讀取另一個(gè)事務(wù)未被執(zhí)行狀態(tài)。這個(gè)過程被稱作臟讀
2. Rocksdb實(shí)現(xiàn)的隔離級(jí)別
2.1 常見的四種隔離級(jí)別
ReadUncommited讀取未提交內(nèi)容,所有事務(wù)都可以看到其他未提交事務(wù)的執(zhí)行結(jié)果,存在臟讀ReadCommited讀取已提交內(nèi)容,事務(wù)只能看到其他已提交事務(wù)的更新內(nèi)容,多次讀的時(shí)候可能讀到其他事務(wù)更新的內(nèi)容RepeatableRead可重復(fù)讀,確保事務(wù)讀取數(shù)據(jù)時(shí),多次操作會(huì)看到同樣的數(shù)據(jù)行(innodb引擎使用快照隔離來實(shí)現(xiàn))。Serializability可串行化,強(qiáng)制事務(wù)之間的執(zhí)行是有序的,不會(huì)互相沖突。
2.2 Rocksdb 支持的隔離級(jí)別及基本實(shí)現(xiàn)
2.2.1 ReadComitted 隔離級(jí)別的測(cè)試
Rocksdb支持ReadCommited的隔離級(jí)別,它能夠提供兩個(gè)保障
- 從數(shù)據(jù)庫(kù)讀時(shí),只能看到已提交的數(shù)據(jù)(沒有臟讀(dirty reads):不同事務(wù)之間能夠讀到對(duì)方未提交的內(nèi)容)
- 寫入數(shù)據(jù)庫(kù)時(shí),只會(huì)覆蓋已經(jīng)寫入的數(shù)據(jù)(沒有臟寫(dirty writes):不同事務(wù)之間的寫在提交之前能夠相互覆蓋)
先看一下簡(jiǎn)單的測(cè)試代碼:
//支持事務(wù)的方式打開rocksdbStatus s = TransactionDB::Open(options, txn_db_options, kDBPath, &txn_db);// 開啟事務(wù)操作,定義當(dāng)前事務(wù)為t1Transaction* txn = txn_db->BeginTransaction(write_options);assert(txn);// 先下發(fā)一個(gè)t1的讀操作s = txn->Get(read_options, "abc", &value);assert(s.IsNotFound());// 再下發(fā)一個(gè)t1的寫操作(注意此時(shí)是在同一個(gè)事務(wù)t1內(nèi)部,現(xiàn)在只是不同的操作)s = txn->Put("abc", "def");assert(s.ok());// 在當(dāng)前事務(wù)外部下發(fā)一個(gè)t2讀操作,確認(rèn)是否存在臟讀(txn_db->Get是一個(gè)不同于當(dāng)前事務(wù)的獨(dú)立事務(wù),t2)s = txn_db->Get(read_options, "abc", &value);std::cout << "t2 Get result " << s.ToString() << std::endl;// 在當(dāng)前事務(wù)外部下發(fā)一個(gè)t3寫操作,這里更新的是不同的key,如果更新相同的key。則t1事務(wù)commit的時(shí)候會(huì)報(bào)錯(cuò)//s = txn_db->Put(write_options, "xyz", "zzz");s = txn_db->Put(write_options, "abc", "zzz");std::cout << "t3 Put result " << s.ToString() << std::endl;// 提交t1事務(wù)s = txn->Commit();assert(s.ok());//提交之后再get一次s = txn_db->Get(read_options, "abc", &value);std::cout << "t4 Get result after commit: " << value << std::endl;delete txn;
輸出如下:
# 兩個(gè)事務(wù)Get時(shí)不可見對(duì)方未提交內(nèi)容,不存在臟讀
t2 Get result NotFound:
# 在提交之后能夠發(fā)現(xiàn)Set的結(jié)果也并未生效,不存在臟寫,切Put相同的key發(fā)現(xiàn)加鎖超時(shí)
t3 Put result Operation timed out: Timeout waiting to lock key
# t4在t1提交之后get t1的結(jié)果的時(shí)候能夠看到t1的結(jié)果生效
t4 Get result after commit def
通過這個(gè)簡(jiǎn)單的測(cè)試代碼以及對(duì)應(yīng)的輸出結(jié)果,我們能夠看出當(dāng)前Rocksdb已經(jīng)能夠支持ReadCommited的隔離級(jí)別,不存在臟讀,同時(shí)臟寫實(shí)現(xiàn)看起來像是通過加鎖來避免的。
2.2.2 ReadCommitted的實(shí)現(xiàn)
簡(jiǎn)單描述一下該隔離特性,Rocksdb的一個(gè)事務(wù)操作是通過Rocksdb內(nèi)部WriteBatch實(shí)現(xiàn)的,針對(duì)不同事務(wù)Rocksdb會(huì)為其分配對(duì)應(yīng)的WriteBatch,由WriteBatch來處理具體的寫入。同時(shí)針對(duì)同一個(gè)事務(wù)的讀操作,會(huì)優(yōu)先從當(dāng)前事務(wù)的WriteBatch中讀,來保證能夠讀到當(dāng)前寫操作之前未提交的更新。提交的時(shí)候則依次寫入WAL和memtable之中,保證ACID的原子性和一致性。
大體的流程如下2.1圖
圖2.1 通過WriteBatch實(shí)現(xiàn) ReadCommitted
以上過程結(jié)合我們的測(cè)試代碼,可以有兩種方式來進(jìn)行
- 顯式得通過事務(wù)的方式寫入,提交
Transaction* txn = txn_db->BeginTransaction(write_options); txn->Get(read_option,"abc",&value); txn->Put("abc","value1"); txn->commit(); - 直接通過TransactionDB生成一個(gè)auto transaction,transactionDB會(huì)將這個(gè)單獨(dú)的操作封裝成事務(wù),并自動(dòng)commit。
txn_db->Get(read_options, "abc", &value); txn_db->Put(write_options, "abc", "zzz");
一種transactionDB這里沒有鎖的沖突檢查,而我們使用transaction的方式進(jìn)行Put,實(shí)驗(yàn)代碼中也能看到有鎖的超時(shí)檢查.
2.2.3 RepeatableRead的實(shí)現(xiàn)
可重復(fù)讀是指Rocksdb重復(fù)多次讀取數(shù)據(jù)的時(shí)候,能夠訪問到預(yù)期的數(shù)值,而不會(huì)被其他事務(wù)的更新操作影響。
這里的可重復(fù)讀其實(shí)在SQL指定標(biāo)準(zhǔn)之前是用快照隔離來描述的,通用的關(guān)系型數(shù)據(jù)庫(kù)都使用MVCC機(jī)制來進(jìn)行多版本管理,多版本的訪問也就是通過快照來進(jìn)行的。
Rocksdb這里的實(shí)現(xiàn)是通過為每一個(gè)寫入的key-value請(qǐng)求添加一個(gè)LSN(Log Sequence Number),最初是0,每次寫入+1,達(dá)到全局遞增的目的。同時(shí)當(dāng)實(shí)現(xiàn)快照隔離時(shí),通過Snapshot設(shè)置其與一個(gè)lsn綁定,則該snapshot能夠訪問到小于等于當(dāng)前l(fā)sn的k-v數(shù)據(jù),而大于該lsn的key-value是不可見的。
相關(guān)代碼在snapshot_impl.h之中
class SnapshotImpl : public Snapshot {public://lsn numberSequenceNumber number_; ......SnapshotImpl* prev_;SnapshotImpl* next_;SnapshotList* list_; // 鏈表頭指針int64_t unix_time_; //時(shí)間戳// 用于寫沖突的檢查bool is_write_conflict_boundary_;
};
snapshot可以有多個(gè),它的創(chuàng)建和刪除是通過操作一個(gè)全局的雙向鏈表來進(jìn)行,天然得根據(jù)創(chuàng)建的時(shí)間來進(jìn)行排序SetSnapShot()函數(shù)創(chuàng)建一個(gè)快照。
快照隔離的測(cè)試代碼如下:
// 通過設(shè)置set_snapshot=true,來在BeginTransaction的時(shí)候就設(shè)置一個(gè)快照value = "def";txn_options.set_snapshot = true;txn = txn_db->BeginTransaction(write_options, txn_options);//讀取一個(gè)快照const Snapshot* snapshot = txn->GetSnapshot();// 重新生成一個(gè)寫入事務(wù)db->Put(write_options, "abc", "xyz");// 通過讀取的snapshot,來訪問指定的keyread_options.snapshot = snapshot;// 通過GetForUpdate來進(jìn)行讀操作,這個(gè)函數(shù)鎖定多個(gè)事務(wù)操作,即也會(huì)讓之前的Put加入到WriteBatch中。s = txn->GetForUpdate(read_options, "abc", &value);assert(value == "def");// 提交事務(wù)s = txn->Commit();// 新生成的事務(wù)可能與讀操作沖突,不過這里用了GetForUpdate就不會(huì)產(chǎn)生沖突了assert(s.IsBusy());delete txn;// 釋放snapshotread_options.snapshot = nullptr;snapshot = nullptr;
其中用到了GetForUpdate函數(shù),區(qū)別于Get接口,GetForUpdate對(duì)讀記錄加獨(dú)占寫鎖,保證后續(xù)對(duì)該記錄的寫操作是排他的。保證了多個(gè)事務(wù)的操作都能夠被GetForUpdate鎖定,而不是一個(gè)GetForUpdate成功,其他的失敗。
2.2.4 事務(wù)并發(fā)處理
通過對(duì)以上事務(wù)的隔離性的分析,能夠總結(jié)出以下幾種事務(wù)并發(fā)時(shí)Rocksdb的處理方式。
- 如果事務(wù)都是讀操作,不論操作之間是否有交集,都不會(huì)觸發(fā)鎖定
- 如果事務(wù)沖包含讀、寫操作
- 所有的讀事務(wù)都不會(huì)觸發(fā)鎖定,讀的結(jié)果與snapshot請(qǐng)求相關(guān)
- 寫事務(wù)之間不存在交集,則不會(huì)鎖定
- 寫事務(wù)之間存在交集,如果此時(shí)設(shè)置了snapshot,則會(huì)串行提交;如果沒有設(shè)置snapshot,則只執(zhí)行第一個(gè)寫操作,其他的操作都會(huì)失敗。
3. 一些總結(jié)
本文通過探索Rocksdb的事務(wù)機(jī)制 以及描述了事務(wù)的基本實(shí)現(xiàn),讀提交以及可重復(fù)讀的特性基本能夠讓其作為單機(jī)存儲(chǔ)引擎底座,來適配分布式存儲(chǔ)中的ACID特性。
同時(shí)還有一些更加細(xì)粒度的實(shí)現(xiàn)需要探索:
- 像針對(duì)寫事務(wù)的交集如何進(jìn)行沖突檢測(cè)以及如何通過鎖機(jī)制解決沖突。
- 默認(rèn)使用的悲觀鎖以及可以顯式調(diào)用的樂觀鎖 在隔離性的幾個(gè)級(jí)別中是如何生效的。
- 還有2PC(Two-Pharse-Commit)的實(shí)現(xiàn)機(jī)制,以及2PC上層的應(yīng)用場(chǎng)景
不得不說一個(gè)公共的存儲(chǔ)底座實(shí)現(xiàn)是真的不容易,后續(xù)將嘗試手寫一些隔離級(jí)別,來加深對(duì)分布式鎖的理解。
總結(jié)
以上是生活随笔為你收集整理的Rocksdb 事务(一): 隔离性的实现的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《Bigtable:a distribu
- 下一篇: 中华龙舟大赛可以到现场观看吗