myrocks之事務處理

前言

mysql目前支持的事務引擎有innodb,tokudb. rocksdb加入mysql陣營後,mysql支持的事務引擎增加至3個。
myrocks目前支持的事務隔離級別有read-committed和repeatable-read. 同innodb同樣,myrocks也支持MVCC機制。
能夠說,myrocks提供了很好的事務支持,可以知足的通常業務的事務需求。mysql

sequence number

談到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

snapshot是rocksdb的快照信息,snapshot實際就是對應一個sequence number. 
簡單的講,假設snapshot的sequence number爲Sa, 那麼對於此snapshot來講,只能看到sequence number<=sa的記錄,sequence number<=sa的記錄是不可見的。sql

  • snapshot 結構
    snapshot 主要包含sequence number和snapshot建立時間,sequence number 取自當前的sequence number.
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

MVCC

有了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();
  }
  • 隔離級別實現差別
    在read committed隔離級別下,若是一個大事務要更新1000w行,當它更新了前900w行時,
    同時另外一個事務已經更新了後100w行,那麼myrocks會從新獲取快照,再次嘗試更新,這樣 更新的是新提交的數據,也符合read committed邏輯。具體的討論能夠參考最近的issue#340. 而以前的處理方式是直接報死鎖錯誤。
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: 打印日誌而且返回錯誤

  • 鎖示例

    直接看例子

screenshot.png

binlog XA & 2pc

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社區正在持續改進中,一塊兒期待。

相關文章
相關標籤/搜索