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