關注能夠查看更多粉絲專享blog~sql
MVCC指的是一種提升併發的技術。最先的數據庫系統,只有讀讀之間能夠併發,讀寫,寫讀,寫寫都要阻塞。引入多版本以後,只有寫寫之間相互阻塞,其餘三種操做均可以並行,這樣大幅度提升了InnoDB的併發度。在內部實現中,與Postgres在數據行上實現多版本不一樣,InnoDB是在undolog中實現的,經過undolog能夠找回數據的歷史版本。找回的數據歷史版本能夠提供給用戶讀(按照隔離級別的定義,有些讀請求只能看到比較老的數據版本),也能夠在回滾的時候覆蓋數據頁上的數據。在InnoDB內部中,會記錄一個全局的活躍讀寫事務數組,其主要用來判斷事務的可見性。數據庫
- MySQL的大多數事務型存儲引擎實現的其實都不是簡單的行級鎖。基於提高併發性能的考慮, 它們通常都同時實現了多版本併發控制(MVCC)。不只是MySQL, 包括Oracle,PostgreSQL等其餘數據庫系統也都實現了MVCC, 但各自的實現機制不盡相同, 由於MVCC沒有一個統一的實現標準。
- 能夠認爲MVCC是行級鎖的一個變種, 可是它在不少狀況下避免了加鎖操做, 所以開銷更低。雖然實現機制有所不一樣, 但大都實現了非阻塞的讀操做,寫操做也只鎖定必要的行。
- MVCC的實現方式有多種, 典型的有樂觀(optimistic)併發控制 和 悲觀(pessimistic)併發控制。
- MVCC只在 READ COMMITTED 和 REPEATABLE READ 兩個隔離級別下工做。其餘兩個隔離級別夠和MVCC不兼容, 由於 READ UNCOMMITTED 老是讀取最新的數據行, 而不是符合當前事務版本的數據行。而 SERIALIZABLE 則會對全部讀取的行都加鎖。
read view的快照生成時機,正是由於生成時機的不一樣,形成了RC和RR級別的不一樣可見性,RC沒法防止不可重複度,RR能夠作到不可重複度。segmentfault
// 測試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」一塊兒實現的,不只能夠保證可重複讀,還能夠防止部分幻讀。多線程
事務B在事務A執行過程當中,插入一條數據並提交,事務A再次查詢,雖然經過快照讀獲取了undo log裏面的舊記錄(防止了幻讀),可是事務A中執行update/delete都是能夠成功的,並無真正意義上的防止。併發
由於InnoDB中的操做分爲當前讀(current read)/快照讀(snapshot read)。高併發
當前讀:性能
快照讀:普通的select,不包含當前讀中的select。測試
在RR級別下,快照讀是經過MVCC(併發多版本控制)和undo log來實現的;當前讀是經過record lock(記錄鎖)和gap lock(間隙鎖)來實現的。因此快照讀場景下並無真正的防止幻讀,當前讀場景下既支持可重複度也能夠防止幻讀。spa
// 測試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
複製代碼
// 當前讀的場景就是加上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
複製代碼
參考文獻:
- 《高性能MySQ》
- MySQL-InnoDB-MVCC多版本併發控制