Mysql在RC隔離級別下是如何實現讀不阻塞的?

Mysql的RC和RR隔離級別都採用了MVCC技術從而實現讀不阻塞。在RC下,事務A對數據M執行寫入時,事務B可以讀取M,並且讀到的是M的舊值,事務A提交後,事務B可以讀到修改值。在這篇博客里 http://m.imooc.com/article/details?article_id=17290 介紹到,RC級別下每次select都會重新生成read view,因此能讀取其他事務已提交數據。而RR級別下每次事務開始才生成read view且在事務結束之內不再改變,因此只能讀取本事務開啟前其他已提交事務的數據。

那麼假設現在有三個事務1,2,3,3首先update了數據並commit,接著1進行select,此時活躍事務只有2,由於行記錄的事務號是3,3大於2,那麼按照博客里的說法此時該記錄應該對1不可見,1隻能讀取到舊值。可是事實上,經過測試,1讀取到的是3修改後的值。

RC隔離級別在mysql中到底是如何實現的?

update:

希望有人可以像博客里一樣結合mysql源碼,給出RC級別下,事務進行select時的執行流程。


是的,這種問題必須邀請我:D

MySQL在不同isolation下的可見性都是在底層的storage engine里實現的,但是對外的表現都是一致的,這裡以TokuDB的實現來說明下。

A) READ-COMMITTED隔離級別邏輯:

1. 事務系統維護一個全局的live_txn_list,用於記錄還未committed的事務(ID)

2. 事務開始:select--&>begin-txn--&>create-txn-id--&>insert-to-(live_txn_list)

3. 可見性判斷:iterate-mvcc--&>read-committed-isolation--&>(mvcc-txnid)-not-in-(live_txn_list)--&>return-value

READ-COMMITTED實現很簡單,當拿到mvcc某個版本的時候,去全局未提交表(live_txn_list)里查找下,如果不存在說明事務已提交,返回結果則可。

這裡需要注意:在底層storage engine,每次select都會執行一次begin txn。

B) REPEATABLE-READ隔離級別邏輯:

1. 事務系統維護一個全局的live_txn_list,用於記錄還未committed的事務(ID)

2. 事務開始:select--&>begin-txn--&>create-txn-id--&>insert-to-(live_txn_list)--&>clone-(live_txn_list)-to-(snapshot_txn_list)

3. 可見性判斷:iterate-mvcc--&>repeatable-read-isolation--&>(mvcc-txnid)-not-in-(snapshot_txn_list)--&>return-value

REPEATABLE-READ跟READ-COMMITTED的區別就是:每個txn維護一個自己的snapshot_txn_list(一旦txn-begin就不會再變),所以是repeatable的。

C) READ-UNCOMMITTED隔離級別邏輯:

直接遍歷mvcc,取最後一個版本返回

D) SERIALIZABLE更簡單,自己想想...

相關代碼:

1. clone live txn:

https://github.com/XeLabs/ft-index/blob/master/ft/txn/txn_manager.cc#L316

2. 遍歷mvcc:

https://github.com/XeLabs/ft-index/blob/master/ft/ule.cc#L2254

https://github.com/XeLabs/ft-index/blob/master/ft/ule.cc#L2157

3. MVCC版本可見性判斷:

https://github.com/XeLabs/ft-index/blob/master/ft/txn/txn.cc#L726

從個人經驗看,事務系統(含嵌套事務功能)當屬資料庫最複雜的子系統,需要把Jim Gray那本上千頁的&正確的濃縮在幾萬行的代碼里可不是件容易的事情。

所以呢,如果你想實現一個事務子系統玩玩,可以先摸索著寫個原型,遇到困難再翻翻那本神書,否則你連寫第一行代碼的勇氣都沒有。


rr的事務級快照、rc的語句級快照,都是在查詢語句開始時才拿的,而不是begin的時候


對這個問題比較感興趣,花了大半天時間看了下你說的那篇博客,又大概試著看了下mysql 5.7.19的源碼。

對於你的疑問,我覺得是因為那篇博客有一點寫錯了。

2. 當行記錄的事務ID大於當前系統的最大活動id,就是不可見的。

  if (trx_id &>= view-&>low_limit_id) {

    return(FALSE);

  }

作者: mark_rock

鏈接:http://www.imooc.com/article/17290?block_id=tuijian_wz

來源:慕課網

這裡對low_limit_id的理解是不對的,它不是「當前系統的最大活動id」,而應該是當前系統尚未分配的下一個事務id,也就是目前已出現過的事務id的最大值+1。按照這個來理解,你的問題就不復存在了。

如何證明我的說法?來探索下代碼。

innodb引擎對read view的可見性判斷是通過ReadView::changes_visible方法來處理的(可以看下文檔注釋):

看第175行,就是拿數據版本跟m_low_limit_id欄位進行比較,然後直接返回不可見。而m_low_limit_id代表什麼呢?看看定義:

文檔注釋說的跟ReadView::changes_visible的邏輯一致,然而沒啥卵用,還是不知道m_low_limit_id代表啥。看看對m_low_limit_id的賦值(注意方法的文檔注釋):

以及trx_sys_t::max_trx_id的定義:

說下感想。感覺read view的設計挺巧妙的,rr和rc兩個級別可以共用完全同樣的read view邏輯,甚至單從read view來看,rr級別比rc級別更少消耗系統資源,也難怪為啥mysql默認級別是rr。


"那麼假設現在有三個事務1,2,3,3首先update了數據並commit,接著1進行select,此時活躍事務只有2,由於行記錄的事務號是3,3大於2,那麼按照博客里的說法此時該記錄應該對1不可見,1隻能讀取到舊值。可是事實上,經過測試,1讀取到的是3修改後的值。"

加粗文字理解錯誤。判斷是否可以見不是比較事務號大小,而是判斷事務號是否在活躍事務列表中。如果在,則已經提交的操作不可見。

你舉得例子中,3不在活躍事務列表中,所以3的操作可見。


@呵呵一笑百媚生 的答案很正確, 如果單看MySQL資料庫事務各隔離級別加鎖情況--read committed amp;amp; MVCC文中介紹, 很容易理解錯誤, 恰巧rr級別用穩重所說的比較演算法也不會出錯, 但是rc的話, 你如果還那麼簡單理解read view快照, 那就會出問題, 需要仔細看read view中low_limit_id具體什麼, 可以參考 http://mysql.taobao.org/monthly/2017/10/01/ 中提到 「選取所有已提交事務中最大的XID,加1後記錄在xmax中」


看一下MySQL官方文檔裡面關於MVCC的實現,大概能清楚原理。

基本上每個事務有個時間戳,每條記錄也有個時間戳。MVCC在UNDO裡面保留了更新中數據的時間軸,事務只能看到比自己的時間戳小的記錄。


推薦閱讀:

TAG:資料庫 | MySQL | InnoDB | MVCC | 資料庫事務 |