摘要
在這一篇內容中,我將從事務是什么開始,聊一聊事務的必要性。
然后,介紹一下在InnoDB中,四種不同級別的事務隔離,能解決什么問題,以及會帶來什么問題。
最后,我會介紹一下InnoDB解決高並發事務的方式:多版本並發控制。
1 什么是事務
說到事務,一個最典型的例子就是銀行轉賬:假設A和B的余額都是100元,此時A要向B轉賬50元。那么我們的操作流程是這樣的:
- 查詢A的余額,保存在
balance
中,並判斷balance
是否大於50元 - 如果是,則
balance
減去50元,寫回數據庫,然后給B的余額加上50元,寫回數據庫 - 如果不是,返回余額不足
那么問題來了,在第一步查詢之后,如果我們馬上再進行一次轉賬,而此時A的余額還是原來的100元,大於50,系統判斷余額是充足的,轉賬成功。但是在寫回數據庫的時候,A的余額還是50元,而B的余額變成了200元。
相信你也看出來了,問題的核心在於這個流程被人“橫插了一腳”,沒有安安靜靜不被打擾的執行完這個轉賬的流程。
正因為我們希望我們的業務邏輯可以不被打擾,所以我們有了“事務”。
那么,事務需要什么樣的條件呢?
相信你也或多或少的聽過了ACID這一說法。
1.原子性(Atomicity):在通常的語義下,原子性指的是一條語句不可分割。但是在事務中,指的是組成這條事務的所有語句必須要執行完,或者回滾。
2.一致性(Consistency):這里的一致性和我們說的數據一致性,也有些不太一樣。我們說的數據一致性,一般指的是MySQL和Redis中的數據是一致的,又或者是MySQL主庫和從庫中的數據是一致的。但是在這兒通常指的是事務是否產生了非預期的中間狀態或結果。比如上面銀行轉賬的例子,轉賬之前兩個人的余額總數是200元,而轉賬完變成了250元。這就是不符合一致性的。
3.隔離性(Isolation):顧名思義,隔離性指的是事務之間應該是互不影響的。在MySQL里面,事務的隔離被分成了四個級別,我們在后面會詳細介紹。
4.持久性(Duration):這個很容易理解,如果一個事務提交了,數據必須得被保存,而不能丟失。
2 事務的隔離級別
事務的隔離級別從低到高,分為了讀未提交,讀已提交,可重復讀,串行化。
而每個級別的隔離,可能造成的問題有:臟讀,不可重復讀,幻讀。
下面我們來舉例說明,假設我們有一張只有兩個字段的表,然后插入以下數據:
CREATE TABLE `t`(
id int,
v int,
PRIMARY KEY (`id`)
)ENGINE=InnoDB;
insert into t(id, v) values(0, 0)
注意,以下的內容全都是基於只有一行數據(0, 0)
的表t
。
2.1 讀未提交
此時事務的隔離級別為讀未提交:
在圖中可以看出:在T3時刻,事務A查找到的數據是(0, 1),但是后來事務B回滾了,也就造成了(0, 1)這行數據是錯誤的,這被稱為是臟讀。
問題的根源在於,事務A讀到了事務B未提交的數據,這也是事務隔離級別讀未提交所存在的問題。這樣的事務隔離級別,僅僅能夠保證事務的原子性,但是沒有保證事務的隔離性,是最低級別的事務隔離級別。
2.2 讀已提交
知道了上面的問題是因為事務讀取了尚未提交的數據,那么我們讓事務的隔離級別變成讀已提交,也就是說,此時只能讀取已經提交過的事務。那么這樣做的話,我們來看看會有什么問題:
我們知道,在讀已提交這個隔離級別中,只能查找到已經提交的數據。那么在T5時刻,事務B已經提交了,那么他的更改對於事務A是可見的。
也就是說,在T5時刻,事務A查找到的數據是(0, 1)
。但是問題來了,在T2時刻事務A查找到的數據是(0,0)
。這種在同一個事務中,查找同樣的一行數據,卻得到了不同的結果,稱為“不可重復讀”。
在讀已提交這個事務隔離級別中,問題在於沒能保證在同一個事務中查詢結果是不變的。
2.3 可重復讀
既然在上面我們發現了不能夠在一個事務中保持結果不變的這么一個問題,那么我們讓MySQL在事務啟動的那一瞬間,將所有的數據拷貝成一個快照,然后讓這個事務所有的查找都在這個快照上進行。這樣的話,在同一個事務中,所有的查詢都是一致的。
這樣的事務隔離級別,稱為“可重復讀”。
注意,這里的“把所有數據拷貝成一個快照”的說法是不准確的,因為這樣做的話,每啟動一個事務,所需要的存儲空間就得增加一倍,顯然是不可能的。但是你可以先這么理解,在后面的內容我會跟你解釋MySQL是如何做到“快照”這一功能的。
那么,在“可重復讀”這一隔離級別中,又可能會出現什么樣的問題呢?
在T2時刻,事務A得到的結果是這樣的:
id | v |
---|---|
0 | 0 |
值得注意的是,我們在T3時刻,在事務B中也插入了一行v
為0
的數據,但是因為我們使用的是可重復讀這一隔離級別,所以可以推斷,在T5時刻的查找,並不會找到新插入的這一行數據。
也就是說,在T5時刻,查詢結果還是和T2時刻是一樣的:
id | v |
---|---|
0 | 0 |
但是,問題來了。因為此時事務A是不知道事務B的存在的,當事務A發現不存在id
為1
,v
為0
的數據之后,事務A准備插入這一行數據,MySQL會返回這樣的錯誤:
ERROR 1062 (23000): Duplicate entry '1' for key 'PRIMARY'
這個報錯的意思是,主鍵重復了。然后事務A就很迷惑:明明我查到並不存在這一行數據,但是為什么我就是無法插入呢?
這就是幻讀。
原因和解決辦法我會下一篇文章中提到,我們先繼續看看最嚴格的事務隔離級別。
2.4 串行化
串行化,顧名思義,就是事務必須得串行執行。
說得再詳細一點:所有包含寫操作的事務,必須得串行執行。
和其他的隔離級別不同,在串行化中,讀操作都會被加上共享鎖,並遵循嚴格兩階段加鎖協議。
在串行化中,因為事務是按順序執行的,所以不可能會出現上面提到的那些問題。但是問題在於,當事務串行化之后,MySQL不能再並發處理事務了,此時性能極低。
關於鎖的內容,我將在下一篇提到。
3 多版本並發控制
在2.3 可重復讀內容中,我提到了“快照”這一說法。
不過說的不夠准確,因為MySQL確實不可能在事務啟動的一瞬間將所有的數據都備份一遍。
在這里,我准備介紹一下InnoDB的多版本並發控制(Multi-Version Concurrency Control),簡稱MVCC。
首先明確兩個概念:
首先,每一個事務在啟動的時候都被分配了一個id,這個id由InnoDB分配,是遞增的。
其次,InnoDB會向數據庫中的每一行都添加三個字段,DB_TRX_ID
表示插入或者更新這一行的事務id;DB_ROLL_PTR
是一個指針,指向了undo log
中的舊版本數據;DB_ROW_ID
是一個遞增的行id。
我們先來看這張圖:
還是上面提到的表t,他有兩個字段,id
和v
。然后加上了InnoDB自動添加的指針字段和事務id字段,省略了行id字段。
在最上面的虛線方框外的那行數據,代表了最新的id
為0
的數據,此時的v
為4
,這行數據是由id為50的事務更改的。
往下看,在這個最新的數據中,指針指向了id
為0
,v
為3
的一行數據,而這行數據是由id為44的事務更改的。
說到這里你可能已經明白了,InnoDB每次更新數據,都會把更新這行數據所在的事務的id記錄在事務id
字段中,然后把原數據的內存地址填入指針
字段。也就是說,InnoDB可以根據這里的指針地址,找到這一行數據的修改歷史記錄以及產生這條記錄的事務id。
那么這跟我們說的“快照”,有什么關系呢?
假設現在是“可重復讀”的事務隔離級別,那么在事務啟動的時候,InnoDB內部會生成一個數組,數組里面記錄了所有當前活躍(也就是說還在執行沒有提交)的事務id,並進行排序。
那么在當前事務執行查找語句的時候,找到的每一行數據都會進行如下的判斷:
- 如果這行數據的事務id小於數組中的最小值,那么表示這行數據已經在事務啟動之前更新完畢,可以直接返回
- 如果這行數據的事務id大於數組中的最大值,那么說明這行數據是在當前事務之后啟動並修改的,那么這行數據不可見,需要使用指針找上一條數據,直到符合條件返回
- 如果這行數據的事務id位於數組中的最大最小值中間,那么還需要判斷這行數據的事務id是否在數組中,如果在,代表了這個事務還是活躍的,應該使用指針找上一條數據;否則的話,說明這個事務已經提交了,可以直接返回數據
我們來看一個例子:
假設在此之前,表t已經有了這么一行數據,id=0,v=1,是由id為100的事務插入的。
然后假設事務A的id是101,事務B是102,事務C是103。
到了T4時刻,事務C更新了這行數據,數據的歷史版本如下:
然后到了T6時刻,事務B准備更新這行數據。注意,更新的時候,是不管數據的歷史版本的,一定要更新最新的那行數據。這被稱為是“當前讀”,意思是InnoDB的更新、插入、刪除操作,是與快照無關的,必須得更新最新的數據。關於這一部分的內容,在下一節會繼續展開介紹。
於是,變成了這樣:
然后到了T7時刻,准備讀取數據。
在事務B啟動的時候,事務C還沒有啟動,所以數組為[101, 102],而讀取到的數據版本是102,就是事務B自己做的更新,所以這行數據符合要求,返回。
到了T8時刻,事務A准備讀取數據。因為事務A啟動的時候,數組為[101],而當前的數據事務id是102,大於100,不符合要求,所以要查找上一個數據。
但是上一個數據的id是103,也大於101,所以也不符合要求,查找上一行的數據。
最終,找到了事務id為100的這行數據,返回。
簡單的來講就是:
- 未提交的不可見
- 當前事務啟動之后提交的,不可見
- 當前事務修改的,可見
- 當前事務啟動之前提交的,可見
上面的分析過程是基於“可重復讀”,也就是說,視圖是在事務啟動的一瞬間創建的。其實“讀已提交”也是一樣的意思,只不過一致性視圖不是在事務啟動的一瞬間創建的,而是在每一條select語句(也被稱為一致性讀)之前創建的。
還需要補充的是,數據的歷史版本,都被保存在了undo log
中,並且InnoDB會判斷當不需要這些舊版本數據的時候,會清理以釋放空間。
此外,所有對undo log
的更新,都會被保存在redo log
中。
寫在最后
首先,謝謝你能看到這里!
這篇文章鴿了比較久,不好意思,最近事兒實在是太多了。
本來這篇文章打算寫《事務隔離和鎖》的,但是寫着寫着發現內容太多了一些,就打算這篇先把事務隔離相關的內容寫完,下一篇再寫鎖相關的。
如果在這篇文章中有什么是我理解有誤的,或者是我講的不夠清晰的,歡迎一起交流學習!
下一篇很快送上,這次一定不鴿(笑)
PS:如果有其他的問題,也可以在公眾號找到作者。並且,所有文章第一時間會在公眾號更新,歡迎來找作者玩~