掃二維碼與項目經理溝通
我們在微信上24小時期待你的聲音
解答本文疑問/技術咨詢/運營咨詢/技術建議/互聯(lián)網交流
大家好,我是小林。

創(chuàng)新互聯(lián)建站于2013年開始,先為麗水等服務建站,麗水等地企業(yè),進行企業(yè)商務咨詢服務。為麗水企業(yè)網站制作PC+手機+微官網三網同步一站式服務解決您的所有建站問題。
之前寫過一篇 MySQL 的 MVCC 的工作原理,最近有讀者在網站上學習的時候,評論區(qū)指出了一些問題。
而這個知識點很重要,面試太常問了,所以,我就重寫了這篇文章!開車!
這是我的錢包,共有 100 萬元。
今天我心情好,我決定給你的轉賬 100 萬,最后的結果肯定是我的余額變?yōu)?0 元,你的余額多了 100 萬元,是不是想到就很開心?
轉賬這一動作在程序里會涉及到一系列的操作,假設我向你轉賬 100 萬的過程是有下面這幾個步驟組成的:
可以看到這個轉賬的過程涉及到了兩次修改數(shù)據庫的操作。
假設在執(zhí)行第三步驟之后,服務器忽然掉電了,就會發(fā)生一個蛋疼的事情,我的賬戶扣了 100 萬,但是錢并沒有到你的賬戶上,也就是說這 100 萬消失了!
要解決這個問題,就要保證轉賬業(yè)務里的所有數(shù)據庫的操作是不可分割的,要么全部執(zhí)行成功 ,要么全部失敗,不允許出現(xiàn)中間狀態(tài)的數(shù)據。
數(shù)據庫中的「事務(Transaction)」就能達到這樣的效果。
我們在轉賬操作前先開啟事務,等所有數(shù)據庫操作執(zhí)行完成后,才提交事務,對于已經提交的事務來說,該事務對數(shù)據庫所做的修改將永久生效,如果中途發(fā)生發(fā)生中斷或錯誤,那么該事務期間對數(shù)據庫所做的修改將會被回滾到沒執(zhí)行該事務之前的狀態(tài)。
事務是由 MySQL 的引擎來實現(xiàn)的,我們常見的 InnoDB 引擎它是支持事務的。
不過并不是所有的引擎都能支持事務,比如 MySQL 原生的 MyISAM 引擎就不支持事務,也正是這樣,所以大多數(shù) MySQL 的引擎都是用 InnoDB。
事務看起來感覺簡單,但是要實現(xiàn)事務必須要遵守 4 個特性,分別如下:
InnoDB 引擎通過什么技術來保證事務的這四個特性的呢?
這次將重點介紹事務的隔離性,這也是面試時最常問的知識的點。
為什么事務要有隔離性,我們就要知道并發(fā)事務時會引發(fā)什么問題。
MySQL 服務端是允許多個客戶端連接的,這意味著 MySQL 會出現(xiàn)同時處理多個事務的情況。
那么在同時處理多個事務的時候,就可能出現(xiàn)臟讀(dirty read)、不可重復讀(non-repeatable read)、幻讀(phantom read)的問題。
接下來,通過舉例子給大家說明,這些問題是如何發(fā)生的。
如果一個事務「讀到」了另一個「未提交事務修改過的數(shù)據」,就意味著發(fā)生了「臟讀」現(xiàn)象。
舉個栗子。
假設有 A 和 B 這兩個事務同時在處理,事務 A 先開始從數(shù)據庫中讀取小林的余額數(shù)據,然后再執(zhí)行更新操作,如果此時事務 A 還沒有提交事務,而此時正好事務 B 也從數(shù)據庫中讀取小林的余額數(shù)據,那么事務 B 讀取到的余額數(shù)據是剛才事務 A 更新后的數(shù)據,即使沒有提交事務。
因為事務 A 是還沒提交事務的,也就是它隨時可能發(fā)生回滾操作,如果在上面這種情況事務 A 發(fā)生了回滾,那么事務 B 剛才得到的數(shù)據就是過期的數(shù)據,這種現(xiàn)象就被稱為臟讀。
在一個事務內多次讀取同一個數(shù)據,如果出現(xiàn)前后兩次讀到的數(shù)據不一樣的情況,就意味著發(fā)生了「不可重復讀」現(xiàn)象。
舉個栗子。
假設有 A 和 B 這兩個事務同時在處理,事務 A 先開始從數(shù)據庫中讀取小林的余額數(shù)據,然后繼續(xù)執(zhí)行代碼邏輯處理,在這過程中如果事務 B 更新了這條數(shù)據,并提交了事務,那么當事務 A 再次讀取該數(shù)據時,就會發(fā)現(xiàn)前后兩次讀到的數(shù)據是不一致的,這種現(xiàn)象就被稱為不可重復讀。
在一個事務內多次查詢某個符合查詢條件的「記錄數(shù)量」,如果出現(xiàn)前后兩次查詢到的記錄數(shù)量不一樣的情況,就意味著發(fā)生了「幻讀」現(xiàn)象。
舉個栗子。
假設有 A 和 B 這兩個事務同時在處理,事務 A 先開始從數(shù)據庫查詢賬戶余額大于 100 萬的記錄,發(fā)現(xiàn)共有 5 條,然后事務 B 也按相同的搜索條件也是查詢出了 5 條記錄。
接下來,事務 A 插入了一條余額超過 100 萬的賬號,并提交了事務,此時數(shù)據庫超過 100 萬余額的賬號個數(shù)就變?yōu)?6。
然后事務 B 再次查詢賬戶余額大于 100 萬的記錄,此時查詢到的記錄數(shù)量有 6 條,發(fā)現(xiàn)和前一次讀到的記錄數(shù)量不一樣了,就感覺發(fā)生了幻覺一樣,這種現(xiàn)象就被稱為幻讀。
前面我們提到,當多個事務并發(fā)執(zhí)行時可能會遇到「臟讀、不可重復讀、幻讀」的現(xiàn)象,這些現(xiàn)象會對事務的一致性產生不同程序的影響。
這三個現(xiàn)象的嚴重性排序如下:
SQL 標準提出了四種隔離級別來規(guī)避這些現(xiàn)象,隔離級別越高,性能效率就越低,這四個隔離級別如下:
按隔離水平高低排序如下:
針對不同的隔離級別,并發(fā)事務時可能發(fā)生的現(xiàn)象也會不同。
也就是說:
所以,要解決臟讀現(xiàn)象,就要升級到「讀提交」以上的隔離級別;要解決不可重復讀現(xiàn)象,就要升級到「可重復讀」的隔離級別。
不過,要解決幻讀現(xiàn)象不建議將隔離級別升級到「串行化」,因為這樣會導致數(shù)據庫在并發(fā)事務時性能很差。
InnoDB 引擎的默認隔離級別雖然是「可重復讀」,但是它通過next-key lock 鎖(行鎖和間隙鎖的組合)來鎖住記錄之間的“間隙”和記錄本身,防止其他事務在這個記錄之間插入新的記錄,這樣就避免了幻讀現(xiàn)象。
接下里,舉個具體的例子來說明這四種隔離級別,有一張賬戶余額表,里面有一條記錄:
然后有兩個并發(fā)的事務,事務 A 只負責查詢余額,事務 B 則會將我的余額改成 200 萬,下面是按照時間順序執(zhí)行兩個事務的行為:
在不同隔離級別下,事務 A 執(zhí)行過程中查詢到的余額可能會不同:
這四種隔離級別具體是如何實現(xiàn)的呢?
注意,執(zhí)行「開始事務」命令,并不意味著啟動了事務。在 MySQL 有兩種開啟事務的命令,分別是:
這兩種開啟事務的命令,事務的啟動時機是不同的:
接下來詳細說下,Read View 在 MVCC 里如何工作的?
我們需要了解兩個知識:
那 Read View 到底是個什么東西?
Read View 有四個重要的字段:
知道了 Read View 的字段,我們還需要了解聚簇索引記錄中的兩個隱藏列。
假設在賬戶余額表插入一條小林余額為 100 萬的記錄,然后我把這兩個隱藏列也畫出來,該記錄的整個示意圖如下:
對于使用 InnoDB 存儲引擎的數(shù)據庫表,它的聚簇索引記錄中都包含下面兩個隱藏列:
在創(chuàng)建 Read View 后,我們可以將記錄中的 trx_id 劃分這三種情況:
一個事務去訪問記錄的時候,除了自己的更新記錄總是可見之外,還有這幾種情況:
如果記錄的 trx_id 在 m_ids 列表中,表示生成該版本記錄的活躍事務依然活躍著(還沒提交事務),所以該版本的記錄對當前事務不可見。
如果記錄的 trx_id 不在 m_ids 列表中,表示生成該版本記錄的活躍事務已經被提交,所以該版本的記錄對當前事務可見。
這種通過「版本鏈」來控制并發(fā)事務訪問同一個記錄時的行為就叫 MVCC(多版本并發(fā)控制)。
可重復讀隔離級別是啟動事務時生成一個 Read View,然后整個事務期間都在用這個 Read View。
假設事務 A (事務 id 為51)啟動后,緊接著事務 B (事務 id 為52)也啟動了,那這兩個事務創(chuàng)建的 Read View 如下:
事務 A 和 事務 B 的 Read View 具體內容如下:
接著,在可重復讀隔離級別下,事務 A 和事務 B 按順序執(zhí)行了以下操作:
接下來,跟大家具體分析下。
事務 B 第一次讀小林的賬戶余額記錄,在找到記錄后,它會先看這條記錄的 trx_id,此時發(fā)現(xiàn) trx_id 為 50,比事務 B 的 Read View 中的 min_trx_id 值(51)還小,這意味著修改這條記錄的事務早就在事務 B 啟動前提交過了,所以該版本的記錄對事務 B 可見的,也就是事務 B 可以獲取到這條記錄。
接著,事務 A 通過 update 語句將這條記錄修改了(還未提交事務),將小林的余額改成 200 萬,這時 MySQL 會記錄相應的 undo log,并以鏈表的方式串聯(lián)起來,形成版本鏈,如下圖:
你可以在上圖的「記錄的字段」看到,由于事務 A 修改了該記錄,以前的記錄就變成舊版本記錄了,于是最新記錄和舊版本記錄通過鏈表的方式串起來,而且最新記錄的 trx_id 是事務 A 的事務 id(trx_id = 51)。
然后事務 B 第二次去讀取該記錄,發(fā)現(xiàn)這條記錄的 trx_id 值為 51,在事務 B 的 Read View 的 min_trx_id 和 max_trx_id 之間,則需要判斷 trx_id 值是否在 m_ids 范圍內,判斷的結果是在的,那么說明這條記錄是被還未提交的事務修改的,這時事務 B 并不會讀取這個版本的記錄。而是沿著 undo log 鏈條往下找舊版本的記錄,直到找到 trx_id 「小于」事務 B 的 Read View 中的 min_trx_id 值的第一條記錄,所以事務 B 能讀取到的是 trx_id 為 50 的記錄,也就是小林余額是 100 萬的這條記錄。
最后,當事物 A 提交事務后,由于隔離級別時「可重復讀」,所以事務 B 再次讀區(qū)記錄時,還是基于啟動事務時創(chuàng)建的 Read View 來判斷當前版本的記錄是否可見。所以,即使事物 A 將小林余額修改為 200 萬并提交了事務, 事務 B 第三次讀取記錄時,讀到的記錄都是小林余額是 100 萬的這條記錄。
就是通過這樣的方式實現(xiàn)了,「可重復讀」隔離級別下在事務期間讀到的記錄都是事務啟動前的記錄。
讀提交隔離級別是在每次讀取數(shù)據時,都會生成一個新的 Read View。
也意味著,事務期間的多次讀取同一條數(shù)據,前后兩次讀的數(shù)據可能會出現(xiàn)不一致,因為可能這期間另外一個事務修改了該記錄,并提交了事務。
那讀提交隔離級別是怎么工作呢?我們還是以前面的例子來聊聊。
假設事務 A (事務 id 為51)啟動后,緊接著事務 B (事務 id 為52)也啟動了,接著按順序執(zhí)行了以下操作:
那具體怎么做到的呢?我們重點看事務 B 每次讀取數(shù)據時創(chuàng)建的 Read View。前兩次 事務 B 讀取數(shù)據時創(chuàng)建的 Read View 如下圖:
我們來分析下為什么事務 B 第二次讀數(shù)據時,讀不到事務 A (還未提交事務)修改的數(shù)據?
事務 B 在找到小林這條記錄時,會看這條記錄的 trx_id 是 51,在事務 B 的 Read View 的 min_trx_id 和 max_trx_id 之間,接下來需要判斷 trx_id 值是否在 m_ids 范圍內,判斷的結果是在的,那么說明這條記錄是被還未提交的事務修改的,這時事務 B 并不會讀取這個版本的記錄。而是,沿著 undo log 鏈條往下找舊版本的記錄,直到找到 trx_id 「小于」事務 B 的 Read View 中的 min_trx_id 值的第一條記錄,所以事務 B 能讀取到的是 trx_id 為 50 的記錄,也就是小林余額是 100 萬的這條記錄。
我們來分析下為什么事務 A 提交后,事務 B 就可以讀到事務 A 修改的數(shù)據?
在事務 A 提交后,由于隔離級別是「讀提交」,所以事務 B 在每次讀數(shù)據的時候,會重新創(chuàng)建 Read View,此時事務 B 第三次讀取數(shù)據時創(chuàng)建的 Read View 如下:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-NhC5bZpC-1648719236189)(https://cdn.jsdelivr.net/gh/xiaolincoder/ImageHost4@main/mysql/事務隔離/讀提交事務2.drawio.png)]
事務 B 在找到小林這條記錄時,會發(fā)現(xiàn)這條記錄的 trx_id 是 51,比事務 B 的 Read View 中的 min_trx_id 值(52)還小,這意味著修改這條記錄的事務早就在創(chuàng)建 Read View 前提交過了,所以該版本的記錄對事務 B 是可見的。
正是因為在讀提交隔離級別下,事務每次讀數(shù)據時都重新創(chuàng)建 Read View,那么在事務期間的多次讀取同一條數(shù)據,前后兩次讀的數(shù)據可能會出現(xiàn)不一致,因為可能這期間另外一個事務修改了該記錄,并提交了事務。
事務是在 MySQL 引擎層實現(xiàn)的,我們常見的 InnoDB 引擎是支持事務的,事務的四大特性是原子性、一致性、隔離性、持久性,我們這次主要講的是隔離性。
當多個事務并發(fā)執(zhí)行的時候,會引發(fā)臟讀、不可重復讀、幻讀這些問題,那為了避免這些問題,SQL 提出了四種隔離級別,分別是讀未提交、讀已提交、可重復讀、串行化,從左往右隔離級別順序遞增,隔離級別越高,意味著性能越差,InnoDB 引擎的默認隔離級別是可重復讀。
要解決臟讀現(xiàn)象,就要將隔離級別升級到讀已提交以上的隔離級別,要解決不可重復讀現(xiàn)象,就要將隔離級別升級到可重復讀以上的隔離級別。
而對于幻讀現(xiàn)象,不建議將隔離級別升級為串行化,因為這會導致數(shù)據庫并發(fā)時性能很差。InnoDB 引擎的默認隔離級別雖然是「可重復讀」,但是它通過 next-key lock 鎖(行鎖+間隙鎖的組合)來鎖住記錄之間的“間隙”和記錄本身,防止其他事務在這個記錄之間插入新的記錄,這樣就避免了幻讀現(xiàn)象。
「可重復讀」隔離級別是啟動事務時生成一個 Read View,然后整個事務期間都在用這個 Read View,這樣就保證了在事務期間讀到的數(shù)據都是事務啟動前的記錄。
這兩個隔離級別實現(xiàn)是通過「事務的 Read View 里的字段」和「記錄中的兩個隱藏列」的比對,來控制并發(fā)事務訪問同一個記錄時的行為,這就叫 MVCC(多版本并發(fā)控制)。
在可重復讀隔離級別中,普通的 select 語句就是基于 MVCC 實現(xiàn)的快照讀,也就是不會加鎖的。而 select .. for update 語句就不是快照讀了,而是當前讀了,也就是每次讀都是拿到最新版本的數(shù)據,但是它會對讀到的記錄加上 next-key lock 鎖。

我們在微信上24小時期待你的聲音
解答本文疑問/技術咨詢/運營咨詢/技術建議/互聯(lián)網交流