在這一篇內容中,我將從事務是什麼開始,聊一聊事務的必要性。數據庫
而後,介紹一下在InnoDB中,四種不一樣級別的事務隔離,能解決什麼問題,以及會帶來什麼問題。數組
最後,我會介紹一下InnoDB解決高併發事務的方式:多版本併發控制。併發
說到事務,一個最典型的例子就是銀行轉帳:假設A和B的餘額都是100元,此時A要向B轉帳50元。那麼咱們的操做流程是這樣的:高併發
balance
中,並判斷balance
是否大於50元balance
減去50元,寫回數據庫,而後給B的餘額加上50元,寫回數據庫那麼問題來了,在第一步查詢以後,若是咱們立刻再進行一次轉帳,而此時A的餘額仍是原來的100元,大於50,系統判斷餘額是充足的,轉帳成功。可是在寫回數據庫的時候,A的餘額仍是50元,而B的餘額變成了200元。性能
相信你也看出來了,問題的核心在於這個流程被人「橫插了一腳」,沒有安安靜靜不被打擾的執行完這個轉帳的流程。學習
正由於咱們但願咱們的業務邏輯能夠不被打擾,因此咱們有了「事務」。指針
那麼,事務須要什麼樣的條件呢?code
相信你也或多或少的聽過了ACID這一說法。blog
1.原子性(Atomicity):在一般的語義下,原子性指的是一條語句不可分割。可是在事務中,指的是組成這條事務的全部語句必需要執行完,或者回滾。排序
2.一致性(Consistency):這裏的一致性和咱們說的數據一致性,也有些不太同樣。咱們說的數據一致性,通常指的是MySQL和Redis中的數據是一致的,又或者是MySQL主庫和從庫中的數據是一致的。可是在這兒一般指的是事務是否產生了非預期的中間狀態或結果。好比上面銀行轉帳的例子,轉帳以前兩我的的餘額總數是200元,而轉帳完變成了250元。這就是不符合一致性的。
3.隔離性(Isolation):顧名思義,隔離性指的是事務之間應該是互不影響的。在MySQL裏面,事務的隔離被分紅了四個級別,咱們在後面會詳細介紹。
4.持久性(Duration):這個很容易理解,若是一個事務提交了,數據必須得被保存,而不能丟失。
事務的隔離級別從低到高,分爲了讀未提交,讀已提交,可重複讀,串行化。
而每一個級別的隔離,可能形成的問題有:髒讀,不可重複讀,幻讀。
下面咱們來舉例說明,假設咱們有一張只有兩個字段的表,而後插入如下數據:
CREATE TABLE `t`( id int, v int, PRIMARY KEY (`id`) )ENGINE=InnoDB; insert into t(id, v) values(0, 0)
注意,如下的內容全都是基於只有一行數據(0, 0)
的表t
。
此時事務的隔離級別爲讀未提交:
在圖中能夠看出:在T3時刻,事務A查找到的數據是(0, 1),可是後來事務B回滾了,也就形成了(0, 1)這行數據是錯誤的,這被稱爲是髒讀。
問題的根源在於,事務A讀到了事務B未提交的數據,這也是事務隔離級別讀未提交所存在的問題。這樣的事務隔離級別,僅僅可以保證事務的原子性,可是沒有保證事務的隔離性,是最低級別的事務隔離級別。
知道了上面的問題是由於事務讀取了還沒有提交的數據,那麼咱們讓事務的隔離級別變成讀已提交,也就是說,此時只能讀取已經提交過的事務。那麼這樣作的話,咱們來看看會有什麼問題:
咱們知道,在讀已提交這個隔離級別中,只能查找到已經提交的數據。那麼在T5時刻,事務B已經提交了,那麼他的更改對於事務A是可見的。
也就是說,在T5時刻,事務A查找到的數據是(0, 1)
。可是問題來了,在T2時刻事務A查找到的數據是(0,0)
。這種在同一個事務中,查找一樣的一行數據,卻獲得了不一樣的結果,稱爲「不可重複讀」。
在讀已提交這個事務隔離級別中,問題在於沒能保證在同一個事務中查詢結果是不變的。
既然在上面咱們發現了不可以在一個事務中保持結果不變的這麼一個問題,那麼咱們讓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就很迷惑:明明我查到並不存在這一行數據,可是爲何我就是沒法插入呢?
這就是幻讀。
緣由和解決辦法我會在後面提到,咱們先繼續看看最嚴格的事務隔離級別。
串行化,顧名思義,就是全部的事務必須得串行執行。
在串行化中,由於事務是按順序執行的,因此不可能會出現上面提到的那些問題。可是問題在於,當事務串行化以後,MySQL不能再併發處理事務了,此時性能極低。
在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,並進行排序。
那麼在當前事務執行查找語句的時候,找到的每一行數據都會進行以下的判斷:
咱們來看一個例子:
假設在此以前,表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:若是有其餘的問題,也能夠在公衆號找到做者。而且,全部文章第一時間會在公衆號更新,歡迎來找做者玩~