MySQL MVCC 詳解

轉載自:https://blog.csdn.net/huyuyang6688/article/details/123028254


概述

MVCC 全稱 Mutil-Version Concurrency Control,多版本併發控制,是一種併發控制方法,旨在減少讀寫操作的衝突

我們知道,當有多個事務同時操作數據庫的相同數據時,會出現併發問題,例如,讀 + 寫事務併發可能會導致髒讀、幻讀和不可重複讀等問題,寫+寫事務併發可能會導致數據覆寫等問題

爲了解決讀 + 寫事務併發可能導致的問題,MySQL 的 innodb 引擎實現了 MVCC,做到不用加鎖也可以實現安全的非阻塞的併發讀 + 寫,而對於寫 + 寫事務併發則只能通過加鎖解決


當前讀 + 快照讀

當前讀:當前讀會對讀取的記錄加鎖,保證讀取數據是最新版本,比如:select …… lock in share mode(共享鎖)select …… for update | update | insert | delete(排他鎖)

快照讀:每次修改數據都會在 undo log 記錄原來的數據(保留快照),快照讀就是讀取 undo log 的某一版本的快照,讀取數據可能不是最新版本,比如:select * from t_user where id=1


MVCC 實現原理

1. 隱藏字段

MySQL 每一行記錄除了自定義字段,還有一些隱藏字段:

  • row_id:當表沒定義主鍵時,InnoDB 會以 row_id 爲主鍵生成一個聚集索引
  • trx_id:記錄了新增/最近修改這條記錄的事務 id,事務 id 是自增的
  • roll_pointer:回滾指針指向當前記錄的上一個版本(在 undo log 中)

2. 版本鏈

在修改數據時,會向 undo log 記錄數據原來的快照,除了用於回滾事務,還用於實現 MVCC

用一個簡單的例子來畫一下MVCC 用到的 undo log 版本鏈的邏輯圖:

當事務(trx_id = 100)執行了 insert into t_user values(1,'張三',20)

當事務(trx_id=102)執行了 update t_user set name='李四' where id=1

當事務(trx_id=103)執行了 update t_user set name='王五' where id=1

3. ReadView

在上面的例子中,多個事務對 id=1 的數據修改後,這行記錄除了最新的數據,在 undo log 中還有多個版本的快照。那其他事務查詢時能查到最新版本的數據嗎?如果不能,能讀到哪個版本的快照呢?這就要由 ReadView 來決定了

在對數據進行快照讀時,會產生的一個 ReadView,ReadView 有四個比較重要的變量:

  • m_ids:活躍事務 id 列表,當前系統中所有活躍的(也就是沒提交的)事務的事務 id 列表
  • min_trx_id:m_ids 中最小的事務 id
  • max_trx_id:生成 ReadView 時,系統應該分配給下一個事務的 id,注意不是 m_ids 中最大的事務 id,也就是 m_ids 中的最大事務 id + 1
  • creator_trx_id:生成該 ReadView 的事務的事務 id

某個事務進行快照讀時可以讀到哪個版本的數據,ReadView 有一套算法:

  1. 當【版本鏈中記錄的 trx_id 等於當前事務 id(trx_id = creator_trx_id)】時,說明版本鏈中的這個版本是當前事務修改的,所以該快照記錄對當前事務可見
  2. 當【版本鏈中記錄的 trx_id 小於活躍事務的最小 id(trx_id < min_trx_id)】時,說明版本鏈中的這條記錄已經提交了,所以該快照記錄對當前事務可見
  3. 當【版本鏈中記錄的 trx_id 大於下一個要分配的事務 id(trx_id > max_trx_id)】時,該快照記錄對當前事務不可見
  4. 當【版本鏈中記錄的 trx_id 大於等於最小活躍事務 id】且【版本鏈中記錄的 trx_id 小於下一個要分配的事務 id】(min_trx_id <= trx_id < max_trx_id)時,如果版本鏈中記錄的 trx_id 在活躍事務id列表 m_ids 中,說明生成 ReadView 時,修改記錄的事務還沒提交,所以該快照記錄對當前事務不可見,否則該快照記錄對當前事務可見

當事務對 id=1 的記錄進行快照讀 select * from t_user where id=1,在版本鏈的快照中,從最新的一條記錄開始,依次判斷這四個條件,直到某一版本的快照對當前事務可見,否則繼續比較上一個版本的記錄

MVCC 主要是用來解決 RC 隔離級別下的髒讀和 RR 隔離級別下的不可重複讀的問題,所以 MVCC 只在 RC(解決髒讀)和 RR(解決不可重複讀)隔離級別下生效,也就是 MySQL 只會在 RC 和 RR 隔離級別下的快照讀時纔會生成 ReadView。區別就是,在 RC 隔離級別下,每一次快照讀都會生成一個最新的 ReadView,在 RR 隔離級別下,只有事務中第一次快照讀會生成 ReadView,之後的快照讀都使用第一次生成的 ReadView


手動驗證 MVCC 原理

前提條件:事務(trx_id=100)向表中插入一條的數據並提交了事務:insert into t_user values(1,'張三',20)

之後又有三個事務(事務101、事務102、事務103)對這條數據進行讀寫操作:

時間順序 事務101 事務 102 事務 103
t1 begin
t2 select * from t_user where id=1
t3 begin
t4 select * from t_user where id=1
t5 begin
t6 select * from t_user where id=1
t7 update t_user set name=‘李四’ where id=1
t8 select * from t_user where id=1
t9 select * from t_user where id=1
t10 commit
t11 select * from t_user where id=1
t12 update t_user set name=‘王五’ where id=1
t13 commit
t14 select * from t_user where id=1

在時間點 t1 ~ t6,整個版本鏈中只有一個快照,trx_id 爲 100:

在時間點 t7 ~ t11,整個版本鏈中有兩個快照,trx_id 爲 102、100:

在時間點 t11 ~ t14,整個版本鏈中有三個快照,trx_id 爲 103、102、100:

1. 事務隔離級別爲 RC(讀已提交)

當前事務隔離級別爲 RC(讀已提交)時,每個事務每次查詢對應生成的 ReadView 是這樣的,跟着這張圖來梳理一下:

在時間點 t2,事務 101 查詢時生成的 ReadView 內容爲:

trx_list: 101
min_trx_id:101
max_trx_id:102
creator_trx_id:101

當前時間點,版本鏈中只有一個快照(trx_id = 100),因爲 trx_id(100) < min_trx_id(101),符合算法的第(2)條規則,所以 trx_id = 100 的這個快照對當前事務可見

在時間點 t4,事務 102 查詢時生成的 ReadView 內容爲:

trx_list: 101,102
min_trx_id:101
max_trx_id:103
creator_trx_id:102

當前時間點,版本鏈中只有一個快照(trx_id = 100),因爲 trx_id(100) < min_trx_id(101),符合算法的第(2)條規則,所以 trx_id=100 的這個快照對當前事務可見

在時間點 t6,事務 103 查詢時生成的 ReadView 內容爲:

trx_list: 101,102,103
min_trx_id:101
max_trx_id:104
creator_trx_id:103

當前時間點,版本鏈中只有一個快照(trx_id = 100),因爲 trx_id(100) < min_trx_id(101),符合算法的第(2)條規則,所以 trx_id=100 的這個快照對當前事務可見

在時間點 t8,事務 101 查詢時生成的 ReadView 內容爲:

trx_list: 101,102,103
min_trx_id:101
max_trx_id:104
creator_trx_id:101

當前時間點,版本鏈中有兩個快照(trx_id=102 -> trx_id=100),從版本鏈中的快照中,從最新的開始,依次判斷:

對於 trx_id=102 的快照,因爲 trx_id(102) = creator_trx_id(102),符合算法的第(1)條規則,所以 trx_id=102 的這個快照對當前事務可見

在時間點 t11,事務 103 查詢時生成的 ReadView 內容爲:

trx_list: 101,103
min_trx_id:101
max_trx_id:104
creator_trx_id:103

當前時間點,版本鏈中有兩個快照(trx_id=102 -> trx_id=100),從版本鏈中的快照中,從最新的開始,依次判斷:

對於 trx_id=102 的快照,min_trx_id(101) <= trx_id(102) < max_trx_id(104) ,且 trx_id(102) 不在 trx_list(101,103) 中,說明當前事務生成 ReadView 時,修改該記錄的事務不是活躍事務(已經提交),根據算法的第(4)條規則,trx_id = 102 的快照對當前事務可見。這也就驗證了在 RC 隔離級別下,事務 102 修改且提交的數據對於事務 103 是可見的

在時間點 t14,事務 101 查詢時生成的 ReadView 內容爲:

trx_list: 101
min_trx_id:101
max_trx_id:104
creator_trx_id:101

當前時間點,版本鏈中有三個快照(trx_id=103 -> trx_id=102 -> trx_id=100),從版本鏈中的快照中,從最新的開始,依次判斷:

對於 trx_id = 103 的快照,min_trx_id(101) <= trx_id(103) < max_trx_id(104) ,且 trx_id(103) 不在 trx_list(101) 中,說明當前事務生成 ReadView 時,修改該記錄的事務不是活躍事務(已經提交),根據算法的第(4)條規則,trx_id = 103 的快照對當前事務可見。這也就驗證了在 RC 隔離級別下,事務 103 修改且提交的數據對於事務 101 是可見的

2. 事務隔離級別爲 RR(可重複讀)

當前事務隔離級別爲 RR(可重複讀)時,每個事務每次查詢對應生成的 ReadView 是這樣的,跟着這張圖來梳理一下:

上面說過,在 RC 隔離級別下,每一次快照讀都會生成一個最新的 ReadView;在 RR 隔離級別下,只有事務中第一次快照讀會生成 ReadView,之後的快照讀都使用第一次生成的 ReadView

所以,事務 101 在 t8、t14 時刻查詢時,使用的 ReadView 跟 t2 時刻一樣;事務 102 在 t9 時刻查詢時使用的ReadView 跟 t4 時刻一樣;事務103 在 t11 時刻查詢時使用的 ReadView 跟 t6 時刻一樣

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章