MySQL-InnoDB-MVCC 多版本控制

關注能夠查看更多粉絲專享blog~sql

概述

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能夠作到不可重複度。segmentfault

  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(間隙鎖)來實現的。因此快照讀場景下並無真正的防止幻讀,當前讀場景下既支持可重複度也能夠防止幻讀。spa

快照讀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多版本併發控制
相關文章
相關標籤/搜索