myrocks之事務處理

zysql發表於2016-11-16

title: MySQL · myrocks · myrocks之事務處理

author: 張遠

前言

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

sequence number

談到rocksdb事務,就必須提及rocksdb中的sequence number機制。rocksdb中的每一條記錄都有一個sequence number, 這個sequence number儲存在記錄的key中。

InternalKey: | User key (string) | sequence number (7 bytes) | value type (1 byte) |

對於同樣的User key記錄,在rocksdb中可能存在多條,但他們的sequence number不同。
sequence number是實現事務處理的關鍵,同時也是MVCC的基礎。

snapshot

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

  • 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的建立和刪除都需要維護雙向連結串列。
  • 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時可以清理。

判斷的大體原則是,從全域性雙向連結串列取出最小的snapshot sequence number Sn. 如果已刪除的老記錄sequence number <=Sn, 那麼這些老記錄在compact時可以清理掉。

MVCC

有了snapshot,MVCC實現起來就很順利了。記錄的sequence number天然的提供了記錄的多版本資訊。
每次查詢使用者記錄時,並不需要加鎖。而是根據當前的sequence number Sn建立一個snapshot, 查詢過程中只取小於或等於Sn的最大sequence number的記錄。查詢結束時釋放snapshot.

關鍵程式碼段

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:
       
       ......

隔離級別

隔離級別也是通過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社群正在持續改進中,一起期待。


相關文章