解鎖MySQL的黑科技:事務與隔離
1. 引言
大家好,我是小?,一個漂泊江湖多年的 985 非科班程序員,曾混跡于國企、互聯網大廠和創業公司的后臺開發攻城獅。
最近小?在梳理我之前的面試資料時發現,面試過程中,基本上都會問到 MySQL 數據庫相關的知識點。
而 MySQL 中,問得最多的就是事務、隔離級別以及 MVCC 這幾個,無論是互聯網大廠、小廠,甚至是國企,它們的覆蓋率竟高達 80%。
其實面試官也知道,八股文誰都會背,但是可以說明白,甚至說透徹的候選人卻是鳳毛麟角。
所以今天小?就帶大家來解鎖那些藏在 MySQL 底層的黑科技:事務與隔離。
2、事務
2.1 直播打賞
首先,讓我們來談談事務。
事務就像一場魔法表演,它可以確保一系列數據庫操作要么全部執行成功,要么一點都不執行。
假設你在看直播時,想打賞 500 塊給美女主播,這時需要扣除你的賬戶余額,并同時增加美女主播的賬戶金額。
如果轉賬的兩個操作中的一個失敗,那你就可能損失金錢或者讓金錢消失不見,美女主播也就收不到錢了。
這時,事務就派上用場了。
它可以保證這兩個操作要么同時成功,要么同時失敗,絕不會出現一半成功一半失敗的尷尬局面。
所以,我們總結一下:
Q:數據庫為什么要有事務?
A:為了保證業務正常運轉,數據最終一致。
2.2 事務特性
明白了什么是事務,以及為什么需要事務。
接下來我們聊一聊事務的 4 個特性:原子性、一致性、隔離性和持久性,簡稱 ACID。
原子性(Atomicity)
原子性是指事務包含的操作要么全部成功,要么全部不成功。
比如 A、B 賬戶的初始余額為 800 元,100元。此時,A 向 B 轉賬 500 元,那么分解開來就是 A 賬戶減 500 元,B 賬戶加 500 元。
最終結果是 A 賬戶余額為 300 元,B 賬戶余額為 600 元。這兩個賬戶余額更新的操作,要么全部執行,要么都不執行。
拿給美女主播打賞的例子,原子性可以保證:要么錢還在,要么錢轉到主播賬戶上并收獲主播的一聲謝謝哥哥!
一致性(Consistency)
事務執行前,和執行后都會保持一致性狀態。
A、B 賬戶在轉賬后,會發生兩種情況:
- 錢轉到 B 賬戶里了,這時 A、B 賬戶分別為 300、600 元;
- 錢轉出去的過程中數據庫網絡斷開,事務回滾了,A、B 賬戶還是 800、100 元。
無論怎樣,事務發生前后,A、B 銀行賬戶的總額都應該為 900 元,這就是前后一致性。
隔離性(Isolation)
隔離性是當多個用戶并發訪問數據庫時,不管是不是操作同一個庫、還是同一張表,數據庫為每一個用戶開啟的事務,不能被其他事務的操作所干擾,多個并發事務之間也要相互隔離。
比如,A 向 B 轉賬的時候,不管別人怎么轉賬,都不會影響他們的交易。
圖片
拿給美女主播打賞的例子,隔離性就是:不管有多少人在給主播打賞,都不會影響你轉錢的事務,也就不會影響主播叫你一聲好哥哥!
持久性(Durability)
一個事務一旦被提交了,那么對數據庫中的數據的改變就是持久性的【即保存到磁盤里】,即便是在數據庫系統遇到故障的情況下也不會丟失提交事務的操作。
拿給美女主播打賞的例子,持久性就是:你只要給主播轉了錢,錢就進了她的賬戶,無論收獲主播的多少聲謝謝好哥哥,錢也回不來了。
接下來,我們總結一下:
- Q:為什么事務有這幾大特性?
- A:我們要保證事務的數據一致性,就需要一些手段來實現,這幾種手段就是事務的幾個特性。
它們分別是原子性、一致性、隔離性和持久性,其中一致性是目的,而原子性、一致性和隔離性都是為了實現數據一致性的手段。
3. 事務并發和隔離
事務并發
并發是指計算機系統或程序在同一時間內同時處理多個任務或操作的能力,也就是允許多個用戶進程去處理同一塊臨界區。
想從進程或處理器的角度來理解并發的,可以看我之前的這篇文章:GPM調度模型
拿打賞主播來舉例,并發就是多個觀眾都想打賞主播,如果你們一起轉錢,那主播的賬戶余額該怎么修改呢?
這里的任務就是轉賬,用戶進程就是負責交易的服務器進程,臨界區就是主播賬戶的存儲空間。
如果出現了事務并發,就會帶來一些意想不到的問題,例如常見的臟寫、臟讀、重復讀和幻讀。
臟寫
臟寫是指:在事務并發的時候,一個事務可以修改另外一個正在進行中的事務的數據,這可能會導致一個寫的事務會覆蓋另外一個寫的事務數據。
當你和小帥一起給美女主播打賞時,你打賞了 500 塊,小帥打賞了 1000 塊,在寫入數據庫的時候,你寫入的數據被小帥的數據給覆蓋了。
最后導致的結果就是,你錢沒了,而美女主播在直播間說的是謝謝小帥哥哥的打賞!
事務隔離
500 塊錢沒了,美女主播還不理你,你很傷心,但是不知道怎么辦?
別難過!事務隔離可以幫你。
MySQL 提供了事務隔離級別,包括:讀未提交、讀已提交、可重復讀以及串行化,來解決事務中各種并發問題,專治各種不開心。
RU - 讀未提交(Read uncommitted)
RU(讀未提交)是指,如果一個事務開始寫數據,則另外一個事務不允許同時進行寫操作,但允許其他事務讀取此行數據。
RU 可以排他寫,但是不排斥讀線程實現。
這種隔離級別解決了上面的臟寫問題,但可能會出現臟讀,即事務 B 讀取到了事務 A 未提交的數據。
你想給美女主播打賞 500 塊,發現銀行卡余額只有 300 塊,這時你想到了前幾天找你借了 500 塊錢的小帥,于是讓小帥還錢。
小帥非常清楚數據庫的事物隔離機制,知道你處于 RU 的事務隔離級別。于是說馬上還你錢,這時出現了以下情況:
圖片
- 小帥:開啟事務 A,給你轉錢 500,事務未提交;
- 你:開啟事務 B,查詢余額,發現余額已經加了 500,于是把小帥的借條撕掉,并準備給主播打賞;
- 小帥:看到借條已經沒了,于是撤銷事務 A。他的錢一分沒少,而你只讀到了他事務 A 里的余額,但是真實的余額沒有增加,即發生了臟讀;
- 你:打賞付款時余額不足,損失了價值 500 塊錢的借條。
你非常失望,打算和小帥絕交,然后繼續學習剩下的隔離機制,看看怎么避免臟讀發生。
RC - 讀已提交(Read committed)
該隔離級別在一個事務進行數據寫入時,不允許別的事務對該行數據進行訪問(包括讀寫)。這樣就可以保證事務讀到的數據一定是已經提交了的,解決了臟讀的問題。
但是 RC 會出現不可重復讀的問題,比如:事務 A 需要讀取兩次數據,在讀取完第一次數據后,有另一個事務 B 對該數據進行的更新并提交事務。
此時事務 A 再次讀取該數據時,數據已經發生了改變,即事務中兩次讀取的數據不一致。
小帥為了挽回友情,給你轉了 520 塊錢,但是他覺得只還你 500 塊就可以,所以讓你還他 20 塊錢。
你這會忙著看美女主播,沒有時間轉錢,他建議你把銀行卡的賬號密碼告訴他,他只轉 20。
為了保險起見,你打開了一個事務去查詢銀行卡余額,并告訴了小帥密碼,接下來發生了如下場景:
- 你:開啟事務 A,查詢銀行卡余額為 820;
- 小帥:開啟事務 B,提款 800,并提交了事務 B;
- 你:在事務 A 中再次查詢余額時,發現銀行卡只有 20 塊錢了,發生了不可重復讀。
不僅被借的錢沒拿到,又損失了 280 塊錢,你越想越氣,罵了小帥一頓。然后繼續學習隔離機制,看看怎么防止不可重復讀的問題。
RR - 可重復讀( Repeatable read)
在同一個事務內,多次讀取同一個數據,在這個事務還未結束時,其他事務不能訪問該數據(包括讀寫)。
這種隔離級別下解決了臟讀和不可重復讀的問題,但是可能會出現幻讀。
如事務 A 在多次讀取數據時,有另一個事務 B 在數據行中間插入或刪除了數據,此時事務 A 再次讀取時,可能會發現數據的行數變了。
簡單來說,RR - 可重復讀可以保證當前事務不會讀取到其他事務已提交的 update 操作,但無法感知其他事務的 insert 和 delete 操作。
小帥知道你不會再借錢了,還被你罵了一頓,心中不忿。就想著用你的銀行賬號搞事情,于是發生了接下來的場景:
- 你:開啟事務 A,想查詢一下剛才交易了幾次,事務里看到結果是 2 次;
- 小帥:開啟事務 B,發現已經不能修改你的余額數據,就索性往你的銀行卡里面寫入了 100 次交易記錄,交易金額高達數千萬,提交事務 B;
- 你:在事務 A 里面繼續查詢交易次數,發現變成了 102 次;
這時,警察叔叔找上門了,說有人舉報你惡意洗黑錢,需要協助調查一下。
還好,經過一番解釋和通過銀行數據庫的日志調查,發現是有人惡意篡改交易記錄,你平安無事回到了家。
這時,你痛定思痛,驚覺交友不慎!于是沉下心來繼續學習隔離機制。
可串行化(Serializable)
該隔離級別下,事務只能依次執行,解決了臟讀、不可重復讀和幻讀的問題。但是代價較高,性能很低,一般很少使用。
在這種情況下,每次有觀眾和你一樣想給主播打賞,都需要排隊等候,直到前面的交易事務完全結束。
這時,你了解到事務的奇妙和隔離的重要,于是打算好好學習數據庫,不再看美女主播跳舞了。
而小帥,卻迷失在面向局子編程的路上越走越遠。
4. 小結
我們總結一下,數據庫通過隔離級別解決了事務并發出現的各種問題:
- RU,讀未提交解決了臟寫問題,但可能出現臟讀;
- RC,讀已提交解決了臟讀問題,但可能出現不可重復讀;
- RR,可重復讀解決了不可重復讀的問題,但可能出現幻讀;
- Serializable,串行化解決了幻讀的問題,但性能很低。
MySQL 是怎么實現事務隔離性的呢?
答案是加鎖。事務級別越高,解決的并發事務問題越多,同時也意味著加的鎖就越多。
鎖的個數對比:RU-讀未提交 < RC-讀已提交 < RR-可重復讀 < Serializable-串行化。
但是,頻繁的加鎖可能會導致讀取數據的時候沒辦法修改,修改數據的時候沒辦法讀取,極大的降低了數據庫讀寫性能,就像串行化的隔離級別那樣。
所以,為了權衡數據安全和性能,MySQL 數據庫默認使用的是 RR,即可重復讀的隔離級別。