❝題外話:以前的文章的首發在掘金,以後開始會首發在公衆號了,感謝支持html
❞
下面開始個人文章:mysql
咱們知道,根據MySQL的鎖機制,寫鎖和讀鎖是衝突的,因此MySQl經過MVCC(多版本併發控制)方式來處理讀寫衝突,提升數據庫高併發場景下的吞吐性能web
最先的數據庫系統,只有讀讀之間能夠併發,讀寫、寫讀、寫寫都要阻塞。引入MVCC後,只有寫寫之間相互阻塞,其餘三種操做均可以並行,這樣大幅度提升了InnoDB的併發度。sql
在內部實現中,InnoDB經過undo log保存每條數據的多個版本,而且可以找回數據的歷史版本提供給用戶讀,每一個事務讀到的數據版本多是不同的數據庫
爲何須要MVCC
InnoDB相比MyISAM有兩大特色,一是支持事務,二是支持行級鎖微信
「但併發事務處理也會帶來一些問題,主要包括如下幾種狀況:」併發
更新丟失( Lost Update ):當兩個或多個事務選擇同一行,而後基於最初選定的值更新該行時,因爲每一個事務都不知道其餘事務的存在,就會發生丟失更新問題 ,最後的更新覆蓋了其餘事務所作的更新。編輯器
❝如何避免這個問題呢,最好在一個事務對數據進行更改但還未提交時,其餘事務不能訪問修改同一個數據高併發
❞
髒讀( Dirty Reads ):一個事務正在對一條記錄作修改,在這個事務並提交前,這條記錄的數據就處於不一致狀態;這時,另外一個事務也來讀取同一條記錄,若是不加控制,第二個事務讀取了這些還沒有提交的髒數據性能
❝官網對髒讀定義的地址爲 https://dev.mysql.com/doc/refman/5.7/en/glossary.html#glos_dirty_read
❞
不可重複讀( Non-Repeatable Reads ):一個事務在讀取某些數據已經發生了改變、或某些記錄已經被刪除了
❝官網對不可重複讀定義的地址爲 https://dev.mysql.com/doc/refman/5.7/en/glossary.html#glos_non_repeatable_read
❞
幻讀( Phantom Reads ):一個事務按相同的查詢條件從新讀取之前檢索過的數據,卻發現其餘事務插入了知足其查詢條件的新數據
❝官網對幻讀定義的地址爲 https://dev.mysql.com/doc/refman/5.7/en/glossary.html#glos_phantom
❞
以上是併發事務過程當中會存在的問題,解決更新丟失能夠交給應用,可是後三者須要數據庫提供事務間的隔離機制來解決
「實現隔離機制的方法主要有兩種 :」
1.加讀寫鎖,效率低
2.一致性快照讀,即 MVCC
「注意:」
❝MySQL中 InnoDB 引擎支持 MVCC; 應對高併發事務, MVCC 比單純的加行鎖更有效, 開銷更小; MVCC 在讀已提交(Read Committed)和可重複讀(Repeatable Read)隔離級別下起做用(下面介紹)
❞
MVCC原理
整體上來說MVCC的實現是基於ReadView版本鏈以及Undo日誌實現的
首先對於使用InnoDB存儲引擎的表來講,它的聚簇索引記錄中都包含兩個必要的隱藏列
-
trx_id:每次一個事務對某條聚簇索引記錄進行改動時,都會把該事務的事務id賦值給trx_id隱藏列。 -
roll_pointer:每次對某條聚簇索引記錄進行改動時,都會把舊的版本寫入到undo日誌中,而後這個隱藏列就至關於一個指針,能夠經過它來找到該記錄修改前的信息
❝undo log主要存儲的也是邏輯日誌,好比咱們要insert一條數據了,那undo log會記錄的一條對應的delete日誌。咱們要update一條記錄時,它會記錄一條對應相反的update記錄
❞
❝聚簇索引簡單說就是表中記錄的物理順序與鍵值的索引順序相同,一個表只能有一個彙集索引,這類索引是和數據存在一塊兒的
❞
如今比方說咱們的表test如今只包含一條記錄
CREATE TABLE t_test (
id INT,
name VARCHAR(100),
PRIMARY KEY (id)
) Engine=InnoDB CHARSET=utf8;
mysql> SELECT * FROM t_test;
+--------+--------+
| id | name |
+--------+--------+
| 1 | 月伴飛魚 |
+--------+--------+
1 row in set (0.07 sec)
假設插入該記錄的事務id爲10,那麼此刻該條記錄的示意圖以下所示:
假設以後事務id分別爲20和30對這條記錄進行UPDATE操做
update t_test set name = '周星馳' where id = 1;
update t_test set name = '周星星' where id = 1;
每次對記錄進行改動,都會記錄一條undo日誌,每條undo日誌也都有一個roll_pointer
屬性(INSERT操做對應的undo日誌沒有該屬性,由於該記錄並無更早的版本),能夠將這些undo日誌都連起來,串成一個鏈表,像下圖同樣:
對該記錄每次更新後,都會將舊值放到一條undo日誌中,就算是該記錄的一箇舊版本,隨着更新次數的增多,全部的版本都會被roll_pointer
屬性鏈接成一個鏈表,咱們把這個鏈表稱之爲版本鏈,版本鏈的頭節點就是當前記錄最新的值
ReadView
對於使用READ COMMITTED和REPEATABLE READ隔離級別的事務來講,都必須保證讀到已經提交了的事務修改過的記錄,也就是說假如另外一個事務已經修改了記錄可是還沒有提交,是不能直接讀取最新版本的記錄的
「核心問題就是:須要判斷一下版本鏈中的哪一個版本是當前事務可見的」
所以,爲了解決READ COMMITED和REPEATABLE READ級別下讀取數據的問題,INNODB的設計者提出了READVIEW的概念,READVIEW中包含如下幾個參數:
-
m_ids:表示在生成READVIEW時當前系統中活躍的讀寫事務的事務id列表,活躍的是指當前系統中那些還沒有提交的事務;
-
min_trx_id:表示在生成READVIEW時當前系統中活躍的讀寫事務中最小的事務id,也就是m_ids中的最小值;
-
max_trx_id:表示生成READVIEW時系統中應該分配給下一個事務的事務id值,因爲事務id通常是遞增分配的,因此max_trx_id就是m_ids中最大的那個id再加上1;
-
creator_trx_id:表示生成該READVIEW的事務id,因爲只有在對錶中記錄作改動(增刪改)時纔會爲事務分配事務id,因此在一個讀取數據的事務中的事務id默認爲0;
有了這個READVIEW,就能夠在訪問某條記錄時,按照以下的規則進行判斷就能夠肯定版本鏈中哪一個版本對當前讀事務是否可見:
-
版本的 trx_id==READVIEW中的creator_trx_id
,表示當前讀事務正在讀取被本身修改過的記錄,該版本能夠被當前事務訪問; -
版本 trx_id < min_trx_id
,代表生成該版本的事務在當前事務生成READVIEW前已經提交了,因此該版本能夠被當前事務訪問; -
版本的 trx_id > max_trx_id
,代表生成該版本的事務在當前事務生成READVIEW後纔開啓的,該版本不可被當前事務訪問; -
版本的 trx_id在READVIEW的min_trx_id和max_trx_id之間
,那就須要判斷一下trx_id屬性值是否是在m_ids中。若是在這個範圍內,說明建立READVIEW時該事務還處於活躍狀態,該版本不能夠被當前事務訪問;若是不在,說明建立READVIEW時生成該版本的事務已經被提交,該版本能夠被當前事務訪問;
若是某個版本的數據對當前事務不可見的話,那麼就順着版本鏈找到下一個版本的數據,繼續按照上面的規則繼續進行判斷,以此類推,如果到了最後一個版本,該版本的數據仍對當前事務不可見,那麼就代表該條記錄對該事務徹底不可見,查詢結果就不會包含該條記錄。
「下面說一下,READ COMMITED和REPEATABLE READ在生成READVIEW時的區別:」
RC在每一次 SELECT 語句前都會生成一個 ReadView,事務期間會更新,所以在其餘事務提交先後所獲得的 m_ids 列表可能發生變化,使得先前不可見的版本後續又忽然可見了。
而 RR 只在事務的第一個 SELECT 語句時生成一個 ReadView,事務操做期間不更新
MVCC可否徹底解決幻讀
假設有以下場景:
# 事務T1,REPEATABLE READ隔離級別下
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT * FROM t_test WHERE id = 2;
Empty set (0.01 sec)
# 此時事務T2執行了:INSERT INTO t_test VALUES(2, '呵呵'); 並提交
mysql> UPDATE t_test SET name = '哈哈' WHERE id = 2;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> SELECT * FROM t_test WHERE id = 2;
+--------+---------+
| id | name |
+--------+---------+
| 2 | 哈哈 |
+--------+---------
1 row in set (0.01 sec)
在REPEATABLE READ隔離級別下,T1第一次執行普通的SELECT語句時生成了一個ReadView,以後T2向表中新插入了一條記錄便提交了,ReadView並不能阻止T1執行UPDATE或者DELETE語句來對改動這個新插入的記錄(由於T2已經提交,改動該記錄並不會形成阻塞),可是這樣一來這條新記錄的trx_id
隱藏列就變成了T1的事務id
以後T1中再使用普通的SELECT語句去查詢這條記錄時就能夠看到這條記錄了,也就把這條記錄返回給客戶端了。由於這個特殊現象的存在,能夠認爲InnoDB中的MVCC並不能完徹底全的禁止幻讀
❝注意:InnoDB引入的間隙鎖(Gap Lock),能夠解決幻讀問題
❞
總結
MVCC就是在使用READ COMMITTD、REPEATABLE READ這兩種隔離級別的事務在執行普通的SELECT操做時訪問記錄的版本鏈的過程,這樣可使不一樣事務的讀-寫、寫-讀操做併發執行,從而提高系統性能;
若是以爲不錯,點個贊再走吧,謝謝
參考:
https://dev.mysql.com/doc/refman/8.0/en/innodb-multi-versioning.html
MySQL是怎樣運行的:從根兒上理解 MySQL
掃描二維碼
獲取更多精彩
月伴飛魚
本文分享自微信公衆號 - 月伴飛魚(gh_c4183eee9eb9)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。