Davids原理探究:MySQL-InnoDB-MVCC 多版本控制

MySQL-InnoDB-MVCC 多版本控制

關注可以查看更多粉絲專享blog~

概述

MVCC指的是一種提高併發的技術。最早的數據庫系統,只有讀讀之間可以併發,讀寫,寫讀,寫寫都要阻塞。引入多版本之後,只有寫寫之間相互阻塞,其他三種操作都可以並行,這樣大幅度提高了InnoDB的併發度。在內部實現中,與Postgres在數據行上實現多版本不同,InnoDB是在undolog中實現的,通過undolog可以找回數據的歷史版本。找回的數據歷史版本可以提供給用戶讀(按照隔離級別的定義,有些讀請求只能看到比較老的數據版本),也可以在回滾的時候覆蓋數據頁上的數據。在InnoDB內部中,會記錄一個全局的活躍讀寫事務數組,其主要用來判斷事務的可見性。

  1. MySQL的大多數事務型存儲引擎實現的其實都不是簡單的行級鎖。基於提升併發性能的考慮, 它們一般都同時實現了多版本併發控制(MVCC)。不僅是MySQL, 包括Oracle,PostgreSQL等其他數據庫系統也都實現了MVCC, 但各自的實現機制不盡相同, 因爲MVCC沒有一個統一的實現標準。
  2. 可以認爲MVCC是行級鎖的一個變種, 但是它在很多情況下避免了加鎖操作, 因此開銷更低。雖然實現機制有所不同, 但大都實現了非阻塞的讀操作,寫操作也只鎖定必要的行。
  3. MVCC的實現方式有多種, 典型的有樂觀(optimistic)併發控制 和 悲觀(pessimistic)併發控制。
  4. MVCC只在 READ COMMITTED 和 REPEATABLE READ 兩個隔離級別下工作。其他兩個隔離級別夠和MVCC不兼容, 因爲 READ UNCOMMITTED 總是讀取最新的數據行, 而不是符合當前事務版本的數據行。而 SERIALIZABLE 則會對所有讀取的行都加鎖。

特性

  1. MVCC是被MySQL中 事務型存儲引擎InnoDB 所支持的;
  2. 應對高併發事務, MVCC比單純的加鎖更高效;
  3. MVCC只在 READ COMMITTED 和 REPEATABLE READ 兩個隔離級別下工作;
  4. MVCC可以使用 樂觀(optimistic)鎖 和 悲觀(pessimistic)鎖來實現;
  5. 各數據庫中MVCC實現並不統一
  6. InnoDB的MVCC是通過在每行記錄後面保存兩個隱藏的列來實現的

事務快照的創建過程:

  1. 查看當前所有的未提交併活躍的事務,存儲在數組中
  2. 選取未提交併活躍的事務中最小的XID,記錄在快照的xmin中
  3. 選取所有已提交事務中最大的XID,+1後記錄在xmax中

read view的快照生成時機,正是因爲生成時機的不同,造成了RC和RR級別的不同可見性,RC無法防止不可重複度,RR可以做到不可重複度。

  1. InnoDB中(默認RR級別),事務在begin/start transaction之後的第一條SELECT語句讀操作後,創建一個快照(read view),並將當前系統中其他活躍事務信息記錄下來。
  2. InnoDB中,RC級別事務中每一條SELECT語句都會創建快照(read view)。

undo log

  1. 當我們對記錄做變更操作時就會產生undo log,Undo記錄默認儲存在系統表空間中,從MySQL5.6開始開闢了獨立的undo表空間。
  2. undo記錄中儲存的是老版本數據,當一個比較早的事務要讀取數據的時候就需要隨着undo鏈尋找滿足其可見性的數據,當undo鏈很長的時候,這個過程是比較耗時的。
  3. INSERT/UPDATE/DELETE,其中INSERT操作在事務提交前只對當前事務可見,產生的undo日誌在事務提交之後直接刪除;UPDATE/DELETE則需要維護多版本信息,所以這兩個操作產生的undo log被歸成一類,即update_undo
  4. insert undo log和update undo log:
    1. insert undo log:事務在insert時產生的undo log,只在回滾時需要,並且事務提交之後可以立即刪除。
    2. update undo log:事務在update/delete時產生的undo log,在回滾/一致性讀時需要,當數據庫使用的快照中不涉及該日誌,纔會被purge線程清理掉。

InnoDB行數據隱藏字段

  1. 6字節DB_TRX_ID(事務ID):用於記錄本行數據最後一次進行修改操作(INSERT/UPDATE)的事務ID。(DELETE在InnoDB看來也是一次UPDATE操作,不會真正的刪除數據)
  2. 7字節DB_ROW_PTR(回滾指針):指向undo log record記錄,儲存着重建改行記錄之前的內容。我之前還在想,多線程併發場景下這個undo log record是怎麼記錄之前的內容的,多份?後來想想InnoDB更新是有排它鎖的,只有一個線程能操作。愚蠢-_-!!!
  3. 6字節ROW_DB_ID:包含一個隨新行插入而單調遞增的行ID,當由InnoDB自動產生聚簇索引的時候使用,但是如果表中設置了唯一主鍵,那麼就不會生成該字段了,因爲唯一主鍵已經可以用來生成聚簇索引了。

SQL示例

// 測試InnoDB RR級別測試,併發修改同一條數據,先修改的rollback,後修改的commit,先修改的能否正確回滾
// 事務A
BEGIN;
START TRANSACTION;
select * from `user` where id = 1;
UPDATE `user` SET Field3 = 3 where id = 1;
select * from `user` where id = 1;
SELECT SLEEP(5);// 休眠5s讓事務B開始執行
ROLLBACK;
// 事務A信息
[SQL]BEGIN;
受影響的行: 0
時間: 0.001s

[SQL]
START TRANSACTION;
受影響的行: 0
時間: 0.001s

[SQL]
select * from `user` where id = 1;
受影響的行: 0
時間: 0.001s

[SQL]
UPDATE `user` SET Field3 = 3 where id = 1;
受影響的行: 1
時間: 0.001s

[SQL]
select * from `user` where id = 1;
受影響的行: 0
時間: 0.001s

[SQL]
SELECT SLEEP(5);
受影響的行: 0
時間: 5.000s

[SQL]
ROLLBACK;
受影響的行: 0
時間: 0.002s

// 事務B
BEGIN;
START TRANSACTION;
select * from `user` where id = 1;
UPDATE `user` SET Field2 = 2 where id = 1;
select * from `user` where id = 1;
COMMIT;
// 事務B信息
[SQL]BEGIN;
受影響的行: 0
時間: 0.000s

[SQL]
START TRANSACTION;
受影響的行: 0
時間: 0.000s

[SQL]
select * from `user` where id = 1;
受影響的行: 0
時間: 0.000s

// 此時事務A執行UPDATE的事務並未commit/rollback所以排它鎖阻塞
[SQL]
UPDATE `user` SET Field2 = 2 where id = 1;
受影響的行: 0
時間: 3.016s

[SQL]
select * from `user` where id = 1;
受影響的行: 0
時間: 0.001s

[SQL]
COMMIT;
受影響的行: 0
時間: 0.002s

在上述過程中undo log狀態如圖所示
執行事務A的時候:
在這裏插入圖片描述
執行事務B的時候:
在這裏插入圖片描述
行數據的DB_TRX_ID和RB_ROLL_PER是最新的事務B數據,此時回滾指針指向事務B快照讀的undo log的信息,事務B的快照讀undo log信息的回滾指針指向事務A的undo log信息,此時形成了類似於鏈表形式的結構,不管有多少事務同時操作一行數據,那麼任一事務需要rollback的時候都可以找到當時修改之前的數據並進行回滾操作。具體最終數據是多少,取決於最後commit的事務修改的數據。

當前讀/快照讀

MySQL的InnoDB默認是RR隔離級別,是通過“行排它鎖 + MVCC”一起實現的,不僅可以保證可重複讀,還可以防止部分幻讀

  1. 事務B在事務A執行過程中,插入一條數據並提交,事務A再次查詢,雖然通過快照讀獲取了undo log裏面的舊記錄(防止了幻讀),但是事務A中執行update/delete都是可以成功的,並沒有真正意義上的防止。

  2. 因爲InnoDB中的操作分爲當前讀(current read)/快照讀(snapshot read)。

  3. 當前讀:

    1. select … lock in share mode;
    2. select … for update;
    3. insert
    4. update
    5. delete
  4. 快照讀:普通的select,不包含當前讀中的select。

在RR級別下,快照讀是通過MVCC(併發多版本控制)和undo log來實現的;當前讀是通過record lock(記錄鎖)和gap lock(間隙鎖)來實現的。所以快照讀場景下並沒有真正的防止幻讀,當前讀場景下既支持可重複度也可以防止幻讀。

快照讀SQL示例

// 測試InnoDB RR級別測試,快照讀場景下是否能真正的防止幻讀,事務A在快照讀場景下能否修改事務B新增的數據
// 數據庫當前有6條數據
// 事務A快照讀
BEGIN;
START TRANSACTION;
select * from `user`;
SELECT SLEEP(5);
select * from `user`;
UPDATE `user` set sex = 2;
select * from `user`;
COMMIT;

// 事務A信息
[SQL]BEGIN;
受影響的行: 0
時間: 0.000s

[SQL]
START TRANSACTION;
受影響的行: 0
時間: 0.001s

[SQL]
select * from `user`;
受影響的行: 0
時間: 0.001s

[SQL]
SELECT SLEEP(5);
受影響的行: 0
時間: 5.001s

[SQL]
select * from `user`;
受影響的行: 0
時間: 0.001s

[SQL]
UPDATE `user` set sex = 2;
受影響的行: 7 // 這裏休眠5s之後事務B已經提交了,但是事務A update的時候影響行數是7行,說明並沒有真正防止幻讀
時間: 0.000s

[SQL]
select * from `user`;
受影響的行: 0
時間: 0.000s

[SQL]
COMMIT;
受影響的行: 0
時間: 0.002s

// 事務B
BEGIN;
START TRANSACTION;
select * from `user`;
INSERT INTO `chat_room`.`user` (`id`, `user_name`, `password`, `sex`) VALUES ('1006', 'name', '000', '0');
select * from `user`;
COMMIT;

// 事務B信息
[SQL]BEGIN;
受影響的行: 0
時間: 0.001s

[SQL]
START TRANSACTION;
受影響的行: 0
時間: 0.000s

[SQL]
select * from `user`;
受影響的行: 0
時間: 0.000s

[SQL]
INSERT INTO `chat_room`.`user` (`id`, `user_name`, `password`, `sex`) VALUES ('1006', 'name', '000', '0');
受影響的行: 1
時間: 0.002s // 事務A執行selelct之後休眠了之後才執行update,所以事務B執行insert的時候可以直接獲取到鎖

[SQL]
select * from `user`;
受影響的行: 0
時間: 0.001s

[SQL]
COMMIT;
受影響的行: 0
時間: 0.001s

當前讀SQL示例

// 當前讀的場景就是加上select ... lock in share mode; select for update這裏會產生排它鎖,在事務A提交事務之前事務B在執行insert操作的時候需要等待
// 事務A當前讀
BEGIN;
START TRANSACTION;
select * from `user` for UPDATE;
SELECT SLEEP(5);
select * from `user`;
UPDATE `user` set sex = 2;
select * from `user`;
COMMIT;
// 事務A信息
[SQL]BEGIN;
受影響的行: 0
時間: 0.000s

[SQL]
START TRANSACTION;
受影響的行: 0
時間: 0.001s

[SQL]
select * from `user` for UPDATE;
受影響的行: 0
時間: 0.001s

[SQL]
SELECT SLEEP(5);
受影響的行: 0
時間: 5.001s

[SQL]
select * from `user`;
受影響的行: 0
時間: 0.000s

[SQL]
UPDATE `user` set sex = 2;
受影響的行: 6 // 當前讀場景下只會影響6行,支持可重複讀也可以防止幻讀
時間: 0.000s

[SQL]
select * from `user`;
受影響的行: 0
時間: 0.000s

[SQL]
COMMIT;
受影響的行: 0
時間: 0.002s

// 事務B
BEGIN;
START TRANSACTION;
select * from `user`;
INSERT INTO `chat_room`.`user` (`id`, `user_name`, `password`, `sex`) VALUES ('1006', 'name', '000', '0');
select * from `user`;
COMMIT;

[SQL]BEGIN;
受影響的行: 0
時間: 0.001s

[SQL]
START TRANSACTION;
受影響的行: 0
時間: 0.000s

[SQL]
select * from `user`;
受影響的行: 0
時間: 0.000s

[SQL]
INSERT INTO `chat_room`.`user` (`id`, `user_name`, `password`, `sex`) VALUES ('1006', 'name', '000', '0');
受影響的行: 1
時間: 3.464s // 因爲我們在執行當前讀的時候是表鎖,所以事務B insert需要等待鎖釋放,如果將select * from `user` for UPDATE;修改爲select * from `user` where id = 1000 for UPDATE;那麼無法防止幻讀,事務B不會阻塞,事務A還是會update 7條,這裏面涉及到共享鎖、排它鎖、和間隙鎖後面可以專門再寫篇blog細說

[SQL]
select * from `user`;
受影響的行: 0
時間: 0.001s

[SQL]
COMMIT;
受影響的行: 0
時間: 0.002s

參考文獻:

  1. 《高性能MySQ》
  2. MySQL-InnoDB-MVCC多版本併發控制
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章