Mysql如何實現隔離級別 - 可重複讀和讀提交 源碼分析

Abstract

本文會(1) 演示Mysql的兩種隔離級別.  (2) 跟着mysql的源代碼來看看它是怎麼實現這兩種隔離級別的.

 

Mysql的隔離級別

當有多個事務併發執行時, 我們需要考慮他們之間的相互影響. 比如 事務A寫了數據d, 事務B是否應該看見呢.

Mysql的事務級別包括: read uncommitted -> read commited -> repeatable read -> serializable.

Serializable是最嚴格的一種隔離級別. 類似的也是性能最差的.

read committed 指當前事務A可以看到已經提交的事務B的所有修改. 不管B是否在事務A開始時提交, 還是A開始後, B提交的. 

repeateable read 保證事務A只能看到事務A開始時, 其他事務已經提交的修改和本事務A內的修改.

read uncommitted 可以讀取到未提交的數據.

 

不同隔離級別可能遇到的問題

 

讀未提交

 讀提交

 可重複讀

 串行化

髒讀(讀到沒提交的數據)

不可重複讀(對讀取的某一行發現它的數據變了 比如select * from t where id=1)

幻讀(謂詞帶有where條件的發現讀取的多個記錄不一樣帶來的讀取不一致)

 

例子 : 某事務中, 同一個查詢在不同的時候產生了不同的sets of rows.  
    舉例來說t表id分別有90 和 102的2行.  id列有索引. 比如select * from t where id > 100 for update
             第一次返回了1行, 第二次返回了2行.

無 Mysql中沒有. PostgreSQL可能有

這裏可以看到幻讀和不可重複讀的區別.

 

具體的例子

mysql -uroot -p // 連接
start transaction;
set global transaction isolation level xxxxlevel; // 設置全局
set session transaction isolation level xxxlevel;  // 設置事務隔離級別
// do some ops
commit; // 提交事務
SELECT @@GLOBAL.transaction_isolation, @@SESSION.transaction_isolation; // 查看事務級別

具體的來說可以打開兩個mysql窗口測試. 

 

MySQL如何實現這些隔離級別?

(1) 存儲多個版本 --- 多版本 版本鏈

通用的思想:  因爲可重複讀可能讀到的是事務開始時的數據. 那麼我們知道 數據一定在數據庫中存在了多個版本.

Mysql通過每行的trx_id和roll_pointer實現. Mysql記錄的每一行都會有如下3列:

在內部呈現出如下的一個版本鏈, 那麼當事務需要看到某個版本的時候就可以看到了.(至少有地方存了這些可能還沒提交的數據)

(2) 讀正確的版本 --- ReadView

當我們有了這個版本鏈後, 我們需要確保事務看到的版本數據滿足對應的隔離級別.  

比如有A事務(ID=81), B事務(ID=83), 都是讀提交. 然後上圖中 80, 81, 82已經提交了, 但是200還沒提交.

那麼如果A是可重複讀, 那麼A只能看到82版本的數據 而看不到200版本的數據.

如果A是讀提交, 那麼A可以看到82版本數據 (最新最近提交的).

而這就是通過ReadView實現的.  ReadView對象中存儲了m_ids[]  存儲當前還未提交的事務id.  後面也有介紹.

(3) 防止錯誤的版本寫入  --- 鎖

ReadCommitted 級別 數據只會鎖住讀取的數據. 但是在Repeatable Read中爲了防止幻讀出現, mysql還會鎖住其中的gap.

 

 

源碼解讀

源碼用的我fork的代碼, (主要是添加了一些方便debug和查看的日誌). 編譯方法在這裏

按照官網的說法, 讀提交會每次重新創建一個快照.

Each consistent read, even within the same transaction, sets and reads its own fresh snapshot. For information about consistent reads, see Section 15.7.2.3, “Consistent Nonlocking Reads”.

但是可重複讀只會在事務開始時創建:

This is the default isolation level for InnoDBConsistent reads within the same transaction read the snapshot established by the first read. 

快照在代碼中就是ReadView, 它包含幾個比較重要的值:

 /** The read should not see any transaction with trx id >= this
  value. In other words, this is the "high water mark". */
  trx_id_t m_low_limit_id;

  /** The read should see all trx ids which are strictly
  smaller (<) than this value.  In other words, this is the
  low water mark". */
  trx_id_t m_up_limit_id;

  /** trx id of creating transaction, set to TRX_ID_MAX for free
  views. */
  trx_id_t m_creator_trx_id;

這些值分別決定了當前ReadView/快照中的改動對哪些事務可見. 哪些事務不可見. 

後面我們會根據mysql的代碼來看看是不是這樣的.

一條SQL執行順序大概是這樣: 解析語法 -> 執行 -> 返回.

1. 開始執行:

2. 獲取鎖, 檢查隔離級別.  此時trx (事務) 綁定的 read_view還是NULL

3. 然後for循環讀取表

可以看到MVCC相關的代碼在row_search_mvcc方法中.

4. 事務還沒綁定ReadView. 申請一個新的

ReadView *trx_assign_read_view(trx_t *trx) /*!< in/out: active transaction */
{
  ut_ad(trx->state == TRX_STATE_ACTIVE);

  if (srv_read_only_mode) {
    ut_ad(trx->read_view == NULL);
    return (NULL);

  } else if (!MVCC::is_view_active(trx->read_view)) {
    trx_sys->mvcc->view_open(trx->read_view, trx);
  }

  return (trx->read_view);
}

新生成的ReadView的前面提到的都設置爲當前最大的事務ID:

m_low_limit_no = m_low_limit_id = m_up_limit_id = trx_sys->max_trx_id;

5. 然後就是根據ReadView需要讀取數據了.

這裏 lock_clust_rec_cons_read_sees 會根據當前行的修改的最新事務id(row_get_rec_trx_id)來比較當前事務綁定ReadView是否可以看到最新的修改. 如果不能看到則根據undo日誌恢復到當時的版本返回.

/** Checks that a record is seen in a consistent read.
 @return true if sees, or false if an earlier version of the record
 should be retrieved */
bool lock_clust_rec_cons_read_sees(
    const rec_t *rec,     /*!< in: user record which should be read or
                          passed over by a read cursor */
    dict_index_t *index,  /*!< in: clustered index */
    const ulint *offsets, /*!< in: rec_get_offsets(rec, index) */
    ReadView *view)       /*!< in: consistent read view */
{
  // ......

  /* NOTE that we call this function while holding the search
  system latch. */

  trx_id_t trx_id = row_get_rec_trx_id(rec, index, offsets);

  return (view->changes_visible(trx_id, index->table->name));
}

 

判斷某個ReadView對某個事務的改動是否可見:

讀提交不涉及. 因爲後面可以看到讀提交的ReadView每次都會重新創建.

6. 讀取完成後釋放鎖.  這裏可以看到<=讀提交的會釋放視圖. 那麼事務再有請求的時候就得重新assign view了. 可以看到他們具體的int值. 可重複讀隔離級別視圖不會close. 那麼我們知道同一個事務,  讀提交會在最後關閉read view. 但是可重複讀不會.

7. 經過這樣我們就知道了mysql這2個併發控制的隔離級別到底是怎麼實現的了.

 

總結

1. 爲了在性能和一致性上取得平衡或者有傾向性的選擇, 定義了各種隔離級別.

2. 爲了實現讀提交和可重複讀, Mysql通過多版本鏈 + ReadView來實現這2種隔離級別. MVCC的實現方式相比直接用鎖優勢很大, 讀不需要獲取鎖或者因爲寫操作而被阻塞.

3.可重複讀 -- 事務開始時創建ReadView.  讀提交 -- 事務每個語句都創建新的ReadView

4.Mysql的可重複讀並不會發生幻讀現象. (通過索引記錄鎖 + gap鎖實現的一種next record鎖實現)

 

 

發佈了78 篇原創文章 · 獲贊 30 · 訪問量 25萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章