mysql目前支持的事務引擎有innodb,tokudb. rocksdb加入mysql陣營後,mysql支持的事務引擎增加至3個。
myrocks目前支持的事務隔離級別有read-committed和repeatable-read. 同innodb同樣,myrocks也支持MVCC機制。
能夠說,myrocks提供了很好的事務支持,可以知足的通常業務的事務需求。mysql
談到rocksdb事務,就必須說起rocksdb中的sequence number機制。rocksdb中的每一條記錄都有一個sequence number, 這個sequence number存儲在記錄的key中。git
InternalKey: | User key (string) | sequence number (7 bytes) | value type (1 byte) |
對於一樣的User key記錄,在rocksdb中可能存在多條,但他們的sequence number不一樣。
sequence number是實現事務處理的關鍵,同時也是MVCC的基礎。github
snapshot是rocksdb的快照信息,snapshot實際就是對應一個sequence number.
簡單的講,假設snapshot的sequence number爲Sa, 那麼對於此snapshot來講,只能看到sequence number<=sa的記錄,sequence number<=sa的記錄是不可見的。sql
class SnapshotImpl : public Snapshot { SequenceNumber number_; // sequenct number int64_t unix_time_; // snapshow建立時間 ...... };
snapshot 管理
snapshot由全局雙向鏈表管理,根據sequence number排序。snapshot的建立和刪除都須要維護雙向鏈表。ui
snapshot與compact
rocksdb的compact操做與snapshot有緊密聯繫。以咱們熟悉的innodb爲例,rocksdb的compact相似於innodb的purge操做, 而snapshot相似於InnoDB的read view. innodb作purge操做時會根據已有的read view來判斷哪些undo log能夠purge,而rocksdb的compact操做會根據已有snapshot信息即全局雙向鏈表來判斷哪些記錄在compace時能夠清理。this
判斷的大致原則是,從全局雙向鏈表取出最小的snapshot sequence number Sn. 若是已刪除的老記錄sequence number <=Sn, 那麼這些老記錄在compact時能夠清理掉。spa
有了snapshot,MVCC實現起來就很順利了。記錄的sequence number自然的提供了記錄的多版本信息。
每次查詢用戶記錄時,並不須要加鎖。而是根據當前的sequence number Sn建立一個snapshot, 查詢過程當中只取小於或等於Sn的最大sequence number的記錄。查詢結束時釋放snapshot.unix
關鍵代碼段日誌
DBIter::FindNextUserEntryInternal if (ikey.sequence <= sequence_) { if (skipping && user_comparator_->Compare(ikey.user_key, saved_key_.GetKey()) <= 0) { num_skipped++; // skip this entry PERF_COUNTER_ADD(internal_key_skipped_count, 1); } else { switch (ikey.type) { case kTypeDeletion: case kTypeSingleDeletion: // Arrange to skip all upcoming entries for this key since // they are hidden by this deletion. saved_key_.SetKey( ikey.user_key, !iter_->IsKeyPinned() || !pin_thru_lifetime_ /* copy */); skipping = true; num_skipped = 0; PERF_COUNTER_ADD(internal_delete_skipped_count, 1); break; case kTypeValue: valid_ = true; saved_key_.SetKey( ikey.user_key, !iter_->IsKeyPinned() || !pin_thru_lifetime_ /* copy */); return; case kTypeMerge: ......
隔離級別code
隔離級別也是經過snapshot來實現的。在innodb中,隔離級別爲read-committed時,事務中每的個stmt都會創建一個read view, 隔離級別爲repeatable-read時,只在事務開啓時創建一次read view. rocksdb同innodb相似,隔離級別爲read-committed時,事務中每的個stmt都會創建一個snapshot, 隔離級別爲repeatable-read時,只在事務開啓時第一個stmt創建一次snapshot.
關鍵代碼片斷
rocksdb_commit: if (my_core::thd_tx_isolation(thd) <= ISO_READ_COMMITTED) { // For READ_COMMITTED, we release any existing snapshot so that we will // see any changes that occurred since the last statement. tx->release_snapshot(); }
rocksdb::Status ha_rocksdb::get_for_update( Rdb_transaction* tx, rocksdb::ColumnFamilyHandle* column_family, const rocksdb::Slice& key, std::string* value) const { rocksdb::Status s= tx->get_for_update(column_family, key, value); // If we have a lock conflict and we are running in READ COMMITTTED mode // release and reacquire the snapshot and then retry the get_for_update(). if (s.IsBusy() && my_core::thd_tx_isolation(ha_thd()) == ISO_READ_COMMITTED) { tx->release_snapshot(); tx->acquire_snapshot(false); s= tx->get_for_update(column_family, key, value); } return s; }
innodb不會出現上述狀況,當第一個大事更新是會持有b樹的index lock, 第二個事務會一直等待index lock直至第一個事務提交完成。
myrocks目前只支持一種鎖類型:排他鎖(X鎖),而且全部的鎖信息都保存在內存中。
struct LockInfo { TransactionID txn_id; // Transaction locks are not valid after this time in us uint64_t expiration_time; ...... }
每一個鎖實際是key和LockInfo的映射. 鎖信息都保存在map中
struct LockMapStripe { std::unordered_map<std::string, LockInfo> keys; ...... }
爲了減小全局鎖信息訪問的衝突, rocksdb將鎖信息進行按key hash分區,
struct LockMap { std::vector<LockMapStripe*> lock_map_stripes_; }
同時每一個column family 存儲一個這樣的LockMap.
using LockMaps = std::unordered_map<uint32_t, std::shared_ptr<LockMap>>; LockMaps lock_maps_;
鎖相關參數:
max_num_locks:事務鎖個數限制
expiration:事務過時時間
經過設置以上兩個參數,來控制事務鎖佔用過多的內存。
rocksdb內部實現了簡單的死鎖檢測機制,每次加鎖發生等待時都會向下面的map中插入一條等待信息,表示一個事務id等待另外一個事務id.
同時會檢查wait_txn_map_是否存在等待環路,存在環路則發生死鎖。
std::unordered_map<TransactionID, TransactionID> wait_txn_map_;
死鎖檢測關鍵代碼片斷
TransactionLockMgr::IncrementWaiters: for (int i = 0; i < txn->GetDeadlockDetectDepth(); i++) { if (next == id) { DecrementWaitersImpl(txn, wait_id); return true; } else if (wait_txn_map_.count(next) == 0) { return false; } else { next = wait_txn_map_[next]; } }
死鎖檢測相關參數
deadlock_detect:是否開啓死鎖檢測
deadlock_detect_depth:死鎖檢查深度,默認50
gap lock
innodb中是存在gap lock的,主要是爲了實現repeatable read和惟一性檢查的。
而在rocksdb中,不支持gap lock(rocksdb insert是也會多對惟一鍵加鎖,以防止重複插入,
嚴格的來說也算是gap lock).
那麼在rocksdb一些須要gap lock的地方,目前是報錯和打印日誌來處理的。
相關參數
gap_lock_write_log: 只打印日誌,不返回錯誤
gap_lock_raise_error: 打印日誌而且返回錯誤
鎖示例
直接看例子
myrocks最近也支持了binlog xa.
在開啓binlog的狀況下,myrocks提交時,會經歷兩階段提交階段。
prepare階段,根據server層生成的xid(由MySQLXid+server_id+qurey_id組成),在rockdb內部執行2pc操做,生成Prepare(xid),EndPrepare()記錄。
commit階段,根據事務成仍是失敗,生成Commit(xid)或Rollback(xid)記錄。
rocksdb 2pc參考這裏
myrocks在事務處理方面還有些不完善的地方,好比鎖類型只有單一的X鎖,不支持gap lock,純內存鎖佔用內存等。 myrocks社區正在持續改進中,一塊兒期待。