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