數(shù)據(jù)庫(kù)事務(wù)的ACID特性與事務(wù)隔離級(jí)別小結(jié)

前言

今天是本博客開(kāi)博一周年,一年來(lái)寫(xiě)了將近30萬(wàn)字,收獲頗豐。但是回頭翻看,發(fā)現(xiàn)之前寫(xiě)的“漂在半空中”的東西居多。所以最近幾天打算搞些平時(shí)不會(huì)經(jīng)常注意到,但是又非?;A(chǔ)、重要的知識(shí)點(diǎn),權(quán)當(dāng)復(fù)習(xí)和查漏補(bǔ)缺,以及訓(xùn)練一下自己“說(shuō)人話”的能力。

本文只總結(jié)概念和理論層面的東西,數(shù)據(jù)庫(kù)事務(wù)的具體實(shí)現(xiàn)細(xì)節(jié)會(huì)以MySQL為例另寫(xiě)文章來(lái)說(shuō)(flag立好了_(:з」∠)_

數(shù)據(jù)庫(kù)事務(wù)與ACID特性

在數(shù)據(jù)庫(kù)系統(tǒng)中,一個(gè)事務(wù)(transaction)是指由一系列數(shù)據(jù)庫(kù)操作組成的一個(gè)完整的邏輯過(guò)程??紤]最經(jīng)典的“銀行轉(zhuǎn)賬”情景,從某個(gè)客戶的儲(chǔ)蓄賬戶轉(zhuǎn)賬1000元到理財(cái)賬戶,可以對(duì)應(yīng)以下的事務(wù)描述:

start transaction;
select balance from saving_account where customer_id = 3897;
update saving_account set balance = balance - 1000.00 where customer_id = 3897;
update finance_account set balance = balance + 1000.00 where customer_id = 3897;
commit;

早在1983年的論文《Principles of transaction-oriented database recovery》中,就提出了數(shù)據(jù)庫(kù)事務(wù)必須具備的四大特性是:原子性(atomicity)、一致性(consistency)、隔離性(isolation)、持久性(durability),合稱為ACID。要保證事務(wù)的執(zhí)行是正確而可靠的,就必須滿足ACID。下面我們不按A→C→I→D的順序來(lái),而按更加易于理解的A→D→I→C順序進(jìn)行解說(shuō)。

原子性

一個(gè)事務(wù)必須被視為一個(gè)內(nèi)部不可分的工作單元,以確保該事務(wù)絕對(duì)不會(huì)被部分執(zhí)行——即要么完全執(zhí)行,要么根本不執(zhí)行。當(dāng)事務(wù)中有操作失敗時(shí),所有操作都將回滾,保證數(shù)據(jù)庫(kù)回到事務(wù)前的狀態(tài)。原子性可以說(shuō)是事務(wù)最基本的保障,如果沒(méi)有原子性,假設(shè)上文的SQL語(yǔ)句第4行失敗,但事務(wù)不回滾,仍然提交的話,客戶的儲(chǔ)蓄賬戶就會(huì)白白損失1000元。

MySQL InnoDB存儲(chǔ)引擎使用undo log(回滾日志)機(jī)制實(shí)現(xiàn)原子性。undo log是邏輯日志,記錄了各個(gè)行在已提交事務(wù)修改前的數(shù)據(jù),當(dāng)事務(wù)失敗或主動(dòng)執(zhí)行rollback語(yǔ)句時(shí),根據(jù)undo log的數(shù)據(jù)恢復(fù)事務(wù)前的現(xiàn)場(chǎng)。undo log以及下一節(jié)提到的redo log統(tǒng)稱為MySQL的事務(wù)日志,關(guān)于它們兩個(gè)之后再細(xì)說(shuō)。

持久性

一旦事務(wù)提交,事務(wù)所做的數(shù)據(jù)改變將是永久的。這意味著數(shù)據(jù)改變已經(jīng)被記錄,即使系統(tǒng)崩潰,數(shù)據(jù)也不會(huì)因此而丟失(客戶的錢都還在)。直白地說(shuō),我們總是通過(guò)非易失性存儲(chǔ)設(shè)備(HDD、SSD)來(lái)存儲(chǔ)數(shù)據(jù),只要能夠排除存儲(chǔ)介質(zhì)本身的問(wèn)題,就可以盡量保證物理上的持久性。

但是在實(shí)際的DBMS設(shè)計(jì)中,持久性還要多注意一點(diǎn),就是緩存的持久化。為了提升性能,數(shù)據(jù)的更改往往不是實(shí)時(shí)落盤的,而是先寫(xiě)緩存(如MySQL的buffer pool)再后臺(tái)同步。如果同步未完成而服務(wù)器宕機(jī),數(shù)據(jù)就丟失了。InnoDB使用redo log(重做日志)機(jī)制進(jìn)一步保障持久性。redo log是物理日志,記錄了各個(gè)頁(yè)在已提交事務(wù)修改后的數(shù)據(jù),系統(tǒng)重啟時(shí)可根據(jù)redo log將那些沒(méi)來(lái)得及落盤的數(shù)據(jù)寫(xiě)入。

隔離性

某個(gè)事務(wù)的結(jié)果只有在完成之后才能對(duì)其他事務(wù)可見(jiàn),也即一個(gè)事務(wù)不應(yīng)該影響其它事務(wù)運(yùn)行效果。由于事務(wù)是可以并發(fā)執(zhí)行的,如果這些事務(wù)查詢或修改的是相關(guān)聯(lián)的表和數(shù)據(jù),就有可能會(huì)相互影響,導(dǎo)致不正確的結(jié)果。所以每個(gè)事務(wù)都有各自的完整的數(shù)據(jù)空間,本事務(wù)所做的修改與其他事務(wù)所做的修改要隔離。在查看特定數(shù)據(jù)的更新時(shí),要么看到另一事務(wù)修改之前的狀態(tài),要么看到修改之后的狀態(tài),不會(huì)看到中間狀態(tài)。

仍然以上文轉(zhuǎn)賬事務(wù)為例,當(dāng)DBMS執(zhí)行完第3條SQL,還未執(zhí)行第4條時(shí),如果又有一個(gè)賬戶統(tǒng)計(jì)事務(wù)開(kāi)始運(yùn)行,那么它會(huì)認(rèn)為轉(zhuǎn)賬還未進(jìn)行,即那1000元還在儲(chǔ)蓄賬戶里。如果不隔離的話,統(tǒng)計(jì)事務(wù)在一瞬間就會(huì)看到中間狀態(tài),即1000元憑空消失了。

上面的原子性和持久性都是針對(duì)單個(gè)事務(wù)的,而隔離性是針對(duì)多個(gè)事務(wù)的,所以更復(fù)雜些。在DBMS中實(shí)現(xiàn)隔離性實(shí)際上就是實(shí)現(xiàn)并發(fā)控制(concurrency control)機(jī)制,MySQL InnoDB使用了鎖和MVCC(多版本并發(fā)控制)來(lái)保證隔離性,這兩點(diǎn)之后再說(shuō)。

容易想到,如果一刀切地保證事務(wù)完全隔離,那么DBMS在同一時(shí)間就只能執(zhí)行一個(gè)事務(wù),效率大大降低。所以在實(shí)際操作中會(huì)將隔離性劃分級(jí)別,本文稍后會(huì)總結(jié)。

一致性

數(shù)據(jù)庫(kù)總是從一種一致性狀態(tài)轉(zhuǎn)換到另一種一致性狀態(tài),也即事務(wù)開(kāi)始前和結(jié)束后,數(shù)據(jù)庫(kù)的完整性約束沒(méi)有被破壞。這包含兩個(gè)層面。

  • 數(shù)據(jù)庫(kù)層面:事務(wù)執(zhí)行前后,數(shù)據(jù)都符合預(yù)先設(shè)置的約束,如唯一約束、外鍵約束、enum/check約束、數(shù)據(jù)類型/長(zhǎng)度檢驗(yàn)、觸發(fā)器等。
  • 業(yè)務(wù)層面:事務(wù)執(zhí)行前后,數(shù)據(jù)都在業(yè)務(wù)意義上是正確的。例如開(kāi)頭的轉(zhuǎn)賬過(guò)程,由于是同一客戶內(nèi)部賬戶互轉(zhuǎn),所以客戶的資產(chǎn)總額是不能變的。

之所以把一致性放在最后,是因?yàn)樵有?、持久性和隔離性的設(shè)計(jì)最終都是為了保證一致性而存在的。

小問(wèn)題:ACID特性中的一致性和CAP理論中的一致性有關(guān)聯(lián)嗎?答案是沒(méi)有,它倆并不是同一個(gè)概念。前者是針對(duì)(單機(jī))數(shù)據(jù)庫(kù)事務(wù)的,是內(nèi)部一致性。后者是針對(duì)分布式系統(tǒng)的,是外部一致性。以CAP/BASE理論為代表的分布式基礎(chǔ)對(duì)我們大數(shù)據(jù)工作者來(lái)說(shuō)似乎更重要,最近會(huì)總結(jié)出來(lái)。

事務(wù)的隔離級(jí)別與讀現(xiàn)象

在ANSI/ISO SQL 92標(biāo)準(zhǔn)中,定義了4種事務(wù)隔離級(jí)別,從低到高分別是:未提交讀(READ UNCOMMITTED)、提交讀(READ COMMITTED)、可重復(fù)讀(REPEATABLE READ)、可串行化(SERIALIZABLE)。下面分別解釋它們,并順便引出各個(gè)隔離級(jí)別可能會(huì)出現(xiàn)的其他3種讀現(xiàn)象,即某事務(wù)讀取其他事務(wù)可能修改的數(shù)據(jù)的現(xiàn)象。

未提交讀

最低的隔離級(jí)別,所有事務(wù)都可以看到其他未提交事務(wù)的執(zhí)行結(jié)果。為了簡(jiǎn)單,考慮純粹用鎖實(shí)現(xiàn)隔離性的DBMS,未提交讀級(jí)別的事務(wù)在讀數(shù)據(jù)時(shí)不會(huì)加鎖,在寫(xiě)數(shù)據(jù)時(shí)只會(huì)加行級(jí)共享鎖。

該級(jí)別只比完全不做隔離好一點(diǎn)點(diǎn),幾乎不用在實(shí)際應(yīng)用中。而讀取到未提交的數(shù)據(jù),就稱為臟讀(dirty read)現(xiàn)象。為了解釋它,假設(shè)有以下原始數(shù)據(jù)表。

然后兩個(gè)事務(wù)并發(fā)執(zhí)行如下圖所示的邏輯。

可見(jiàn),事務(wù)2將id = 1的行的age列修改為21,但沒(méi)有提交,造成事務(wù)1查詢到的id = 1的行的age也是21,造成了臟讀。事務(wù)2回滾后,事務(wù)1查詢到的數(shù)據(jù)就是錯(cuò)的了。

小問(wèn)題:臟讀現(xiàn)象雖然多數(shù)情況下不符合預(yù)期,但還是允許存在的,那么臟寫(xiě)(dirty write)允許存在嗎?答案是不行的,如果一個(gè)事務(wù)的修改在未提交時(shí)就被另一個(gè)事務(wù)的修改覆蓋,原子性就被打破了。

提交讀

這是大多數(shù)DBMS(除MySQL外)的默認(rèn)事務(wù)隔離級(jí)別。該級(jí)別其實(shí)就是符合隔離性基本定義的實(shí)現(xiàn):一個(gè)事務(wù)在開(kāi)始時(shí)只能看到其他已提交的事務(wù)所做的改變;一個(gè)事務(wù)從開(kāi)始到提交前,所做的數(shù)據(jù)更改都對(duì)其他事務(wù)不可見(jiàn)。仍然以鎖機(jī)制實(shí)現(xiàn)隔離性為例,該級(jí)別的事務(wù)在讀數(shù)據(jù)時(shí)會(huì)加行級(jí)共享鎖,讀完立即釋放;寫(xiě)數(shù)據(jù)時(shí)會(huì)加行級(jí)排他鎖,直到事務(wù)結(jié)束釋放。

在該級(jí)別下,臟讀現(xiàn)象不會(huì)出現(xiàn),但是需要注意不可重復(fù)讀(non-repeatable read)現(xiàn)象。如下圖示例。

事務(wù)1第一次select和第二次select讀到的id = 1行的age列的值是不同的(因?yàn)槭聞?wù)2中途提交了)。這種同一事務(wù)中,對(duì)同一行數(shù)據(jù)獲取多次,返回不同結(jié)果的現(xiàn)象就是不可重復(fù)讀。

可重復(fù)讀

這是MySQL的默認(rèn)事務(wù)隔離級(jí)別。既然提交讀級(jí)別會(huì)造成不可重復(fù)讀的問(wèn)題,那么在提交讀的基礎(chǔ)上解決該問(wèn)題的隔離級(jí)別自然就叫可重復(fù)讀了。仍然以鎖機(jī)制實(shí)現(xiàn)隔離性為例,該級(jí)別的事務(wù)在讀數(shù)據(jù)時(shí)會(huì)加行級(jí)共享鎖,寫(xiě)數(shù)據(jù)時(shí)會(huì)加行級(jí)排他鎖,兩個(gè)鎖都是直到事務(wù)結(jié)束才釋放。這樣,在事務(wù)1讀某行數(shù)據(jù)到事務(wù)1結(jié)束的整個(gè)過(guò)程中,事務(wù)2肯定無(wú)法修改該行數(shù)據(jù),得到的結(jié)果總是一樣的,problem solved。

但是,該級(jí)別仍然無(wú)法解決最高級(jí)的一種讀現(xiàn)象,即幻讀(phantom read)。如下圖示例。

事務(wù)1執(zhí)行了一個(gè)范圍查詢,第一次執(zhí)行時(shí)返回2條記錄。事務(wù)2向表插入了一條新記錄并直接提交,導(dǎo)致事務(wù)1重新執(zhí)行該范圍查詢時(shí),查到了事務(wù)2插入的那條記錄,并返回3條記錄??梢?jiàn),幻讀實(shí)際上是不可重復(fù)讀在范圍查詢時(shí)的一種特殊情況。

可串行化

最高的隔離級(jí)別。該級(jí)別的事務(wù)在讀數(shù)據(jù)時(shí)會(huì)加行級(jí)共享鎖,對(duì)范圍查詢則會(huì)特別加上范圍鎖,寫(xiě)數(shù)據(jù)時(shí)會(huì)加行級(jí)排他鎖,并且都是直到事務(wù)結(jié)束才釋放,這樣就連幻讀都不會(huì)出現(xiàn)了。如果要保證絕對(duì)安全,即事務(wù)都是嚴(yán)格順序執(zhí)行的,還可以將行級(jí)鎖改為表級(jí)鎖。但同樣地,這個(gè)級(jí)別的并發(fā)性是最低的,只有在強(qiáng)制要求數(shù)據(jù)穩(wěn)定性時(shí),才會(huì)選用它。

The End

強(qiáng)烈推薦《高性能MySQL》(High Performance MySQL)一書(shū),嗯嗯。

民那晚安。祝身體健康,百毒不侵。希望疫情快點(diǎn)過(guò)去。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容