事務(wù)與隔離級別------《Designing Data-Intensive Applications》讀書筆記10

和數(shù)據(jù)庫打交道的程序員繞不開的話題就是:事務(wù),作為一個(gè)簡化訪問數(shù)據(jù)庫的應(yīng)用程序的編程模型。通過使用事務(wù),應(yīng)用程序可以忽略某些潛在的錯(cuò)誤場景和并發(fā)問題,由數(shù)據(jù)庫負(fù)責(zé)處理它們。而并非每個(gè)應(yīng)用程序都需要事務(wù),有時(shí)削弱事務(wù)性擔(dān)?;蛲耆艞壥聞?wù),可以獲得更高的性能或更高的可用性。怎么樣更好的理解數(shù)據(jù)庫中的事務(wù)與隔離級別呢?我們借這篇文章來聊一聊吧~

1.ACID

1983年,Andreas Reuter and Theo H?rder 提出了事務(wù)之中重要的四個(gè)特性:

  • 原子性(Atomicity)
    一般來說,原子指的是不能分解成更小的部分的東西。如果寫操作被組合到一個(gè)原子事務(wù)中,并且由于一個(gè)錯(cuò)誤,事務(wù)不能完成,那么事務(wù)將被中止,數(shù)據(jù)庫必須丟棄或撤消它在該事務(wù)中所做的任何寫入操作。原子性簡化了數(shù)據(jù)庫的數(shù)據(jù)模型:如果一個(gè)事務(wù)被中止時(shí),應(yīng)用程序可以確保它沒有任何改變,因此可以被重試。

  • 一致性(Consistency)
    一致性的表述是:數(shù)據(jù)庫之中的數(shù)據(jù)必須始終正確。例如,在一個(gè)會計(jì)系統(tǒng),所有賬戶的收支必須平衡。應(yīng)用程序有責(zé)任正確定義其事務(wù),從而保持一致性。這不是數(shù)據(jù)庫能保證的:如果你寫了違反你的不變量的壞數(shù)據(jù),數(shù)據(jù)庫不能阻止你。應(yīng)用程序可能會依賴于數(shù)據(jù)庫的原子性和隔離性以達(dá)到一致性。

  • 隔離性(Isolation):
    數(shù)據(jù)庫由多個(gè)客戶端同時(shí)訪問時(shí),如果他們訪問相同的數(shù)據(jù)庫記錄,你會遇到并發(fā)問題。如下圖所示:

    并發(fā)寫對隔離性的破壞

    隔離性意味著并發(fā)執(zhí)行的事務(wù)彼此隔離,數(shù)據(jù)庫確保當(dāng)事務(wù)提交時(shí),結(jié)果與它們順序運(yùn)行相同,即使它們實(shí)際上是并發(fā)運(yùn)行的。

  • 持久性(Durability):
    持久性是一個(gè)承諾,一旦事務(wù)成功提交,它所寫的任何數(shù)據(jù)將不會丟失,即使有硬件故障或數(shù)據(jù)庫崩潰。在單節(jié)點(diǎn)數(shù)據(jù)庫中,持久性通常意味著數(shù)據(jù)已寫入非易失性存儲(如硬盤驅(qū)動器或SSD)。它通常還需要寫入日志,以便出現(xiàn)文件損壞時(shí)恢復(fù)工作。在分布式數(shù)據(jù)庫中,持久性可能意味著數(shù)據(jù)已成功復(fù)制到一些節(jié)點(diǎn)上。

在幾種特性之中,隔離性是DBA對數(shù)據(jù)庫調(diào)優(yōu)最為側(cè)重的部分,接下來,我們著重來聊一聊事務(wù)的隔離性。

2. 隔離級別

如果兩個(gè)事務(wù)不觸及相同的數(shù)據(jù),它們可以安全地并行運(yùn)行,因?yàn)閮烧叨疾灰蕾囉谄渌麛?shù)據(jù)。當(dāng)一個(gè)事務(wù)讀取另一個(gè)事務(wù)同時(shí)修改的數(shù)據(jù),或者兩個(gè)事務(wù)試圖同時(shí)修改同一數(shù)據(jù)時(shí),便會出現(xiàn)并發(fā)問題。

并發(fā)錯(cuò)誤很難通過測試發(fā)現(xiàn),因?yàn)檫@種的錯(cuò)誤觸發(fā)具有偶然性,通常很難重現(xiàn)。并發(fā)性也很難推理,尤其是在大型應(yīng)用程序中,因?yàn)殚_發(fā)人員不一定知道其他代碼片段正在訪問數(shù)據(jù)庫。所以數(shù)據(jù)庫通過提供事務(wù)的隔離性來隱藏應(yīng)用程序開發(fā)者的并發(fā)問題,屏蔽了底層數(shù)據(jù)庫的并發(fā)細(xì)節(jié),提供了一個(gè)串行化的數(shù)據(jù)模型。

天下沒有免費(fèi)的午餐,串行化的隔離級別會帶來額外的性能開銷,所以許多數(shù)據(jù)庫會提供一些弱隔離級別作為選擇,它們可以防止一部分并發(fā)問題。所以,接下來,我們將一一梳理,不同的隔離級別之間的差異。

Read Committed

最基本的隔離級別是Read Committed

  • 當(dāng)從數(shù)據(jù)庫中讀取數(shù)據(jù)時(shí),只看到已提交的數(shù)據(jù)(沒有臟讀)。
  • 當(dāng)寫入數(shù)據(jù)庫時(shí),只覆蓋已提交的數(shù)據(jù)(沒有臟寫)。
臟讀:

一個(gè)事務(wù)已經(jīng)向數(shù)據(jù)庫寫入了一些數(shù)據(jù),但該事務(wù)尚未提交或中止。另一個(gè)事務(wù)可以看到未提交的數(shù)據(jù),就稱為臟讀。Read Committed的隔離級別可以防止臟讀。所以當(dāng)事務(wù)提交之后,事務(wù)中的寫操作才對其他人可見。如下圖所示:

User2在User1事務(wù)提交之后才能讀到新的值

臟寫:

寫操作覆蓋了一個(gè)未提交的值,被稱之為臟寫Read Committed的隔離級別事務(wù)可以防止臟寫,通常是通過延遲寫操作直到前一個(gè)寫事務(wù)已提交或中止時(shí)在繼續(xù)寫入。臟寫會導(dǎo)致數(shù)據(jù)出現(xiàn)不一致,如下圖所示:Alice和Bob要買同一個(gè)東西,臟寫導(dǎo)致了最終的買家是Bob,而發(fā)票卻寄給了Alice。

臟寫導(dǎo)致了數(shù)據(jù)的不一致性

實(shí)現(xiàn):

Read Committed是一種十分流行的隔離級別,許多數(shù)據(jù)庫的默認(rèn)隔離級別便是Read Committed。

數(shù)據(jù)庫通過使用行級鎖防止臟寫:當(dāng)事務(wù)要修改某個(gè)特定行時(shí),它必須首先獲取該行的鎖。然后必須保留該鎖,直到事務(wù)提交或中止為止。只有一個(gè)事務(wù)可以鎖定任何給定行的鎖;如果另一個(gè)事務(wù)要寫入同一個(gè)行,則必須等到第一個(gè)事務(wù)提交或中止后才可獲取鎖并繼續(xù)。

而使用行級鎖避免臟讀會產(chǎn)生很大的代價(jià),容易找出讀延遲。使用當(dāng)事務(wù)正在進(jìn)行時(shí),讀取同一行的任何其他事務(wù)都只給出舊值。只有當(dāng)新值被提交時(shí),事務(wù)才切換到讀取新值。

Read Repeatable

Read Committed看起來是一個(gè)很好的隔離級別了,但是它也會產(chǎn)生一些問題,我們看下面這個(gè)例子:如圖所示,Alice在一家銀行有1000美元的存款,在兩個(gè)賬戶上拆分,每個(gè)賬戶有500美元?,F(xiàn)在,一個(gè)事務(wù)從她的帳戶轉(zhuǎn)到另一個(gè)帳戶100美元。如果她很不幸地在事務(wù)正在進(jìn)行的同一時(shí)刻查看她的賬戶余額清單,她可能會看到一個(gè)賬戶余額在收到的款項(xiàng)到達(dá)之前(余額為500美元),另一個(gè)賬戶在已進(jìn)行的轉(zhuǎn)移之后(新余額為400美元),而100美元消失了。


消失的100美元

在Read Committed隔離級別之下出現(xiàn)的這種異常被稱為不可重復(fù)讀,我們需要尋找新的解決方案。

快照隔離

為了實(shí)現(xiàn)可重復(fù)讀,我們需要快照隔離的技術(shù)。

每個(gè)事務(wù)都從數(shù)據(jù)庫的快照中讀取的,即事務(wù)在事務(wù)開始時(shí)看到數(shù)據(jù)庫中提交的所有數(shù)據(jù)。即使數(shù)據(jù)隨后被另一個(gè)事務(wù)更改,每個(gè)事務(wù)只看到來自特定時(shí)間點(diǎn)的舊數(shù)據(jù)。當(dāng)事務(wù)可以看到數(shù)據(jù)庫的數(shù)據(jù),在特定時(shí)間點(diǎn)被凍結(jié)了。

快照隔離的實(shí)現(xiàn)通常使用寫鎖來防止臟寫,這意味著編寫的事務(wù)可以阻止寫入同一對象的另一個(gè)事務(wù)的進(jìn)程。實(shí)現(xiàn)快照隔離,數(shù)據(jù)庫必須保留數(shù)據(jù)的幾個(gè)不同的提交版本,因?yàn)楦鞣N正在進(jìn)行的事務(wù)可能需要在不同的時(shí)間點(diǎn)查看數(shù)據(jù)庫的狀態(tài),這種技術(shù)被稱為多版本并發(fā)控制(MVCC)。

如下圖所示,每當(dāng)一個(gè)事務(wù)向數(shù)據(jù)庫寫入任何內(nèi)容時(shí),它寫入的數(shù)據(jù)都會用事務(wù)ID進(jìn)行標(biāo)記。


通過事務(wù)ID實(shí)現(xiàn)MVCC

當(dāng)事務(wù)從數(shù)據(jù)庫中讀取時(shí),事務(wù)ID用于決定哪些數(shù)據(jù)可見,哪些數(shù)據(jù)是不可見的。在每次更改值時(shí)創(chuàng)建新版本,數(shù)據(jù)庫可以提供快照隔離,而只產(chǎn)生較小的開銷。

Serializability

Read Repeatable雖然解決了讀取數(shù)據(jù)的問題,但是依然沒有辦法解決并發(fā)寫的問題。我們來看看下面這個(gè)例子:醫(yī)院通常在任何時(shí)候都要有幾個(gè)值班醫(yī)生,必須至少有一位醫(yī)生在值班。醫(yī)生可以調(diào)整他們的輪班,前提是至少有一個(gè)同事在醫(yī)院值班。Alice和Bob是兩位今天值班的醫(yī)生。兩人都想調(diào)整輪班,不幸的是,他們碰巧點(diǎn)擊按鈕大約在同一時(shí)間取消輪班。接下來發(fā)生的情況如圖所示:


并發(fā)寫,產(chǎn)生的問題

由于數(shù)據(jù)庫的隔離級別是快照隔離,兩個(gè)人都檢查到目前有兩個(gè)人值班,因此兩個(gè)事務(wù)都進(jìn)入下一個(gè)階段。Alice認(rèn)為請假沒有問題,Bob也認(rèn)為請假沒有問題。兩個(gè)事務(wù)都提交了,現(xiàn)在沒有醫(yī)生在值班了,數(shù)據(jù)庫的一致性出現(xiàn)了問題。

Serializability 被看作是最強(qiáng)的隔離級別。數(shù)據(jù)庫保證,如果事務(wù)在單獨(dú)運(yùn)行時(shí)行為正確,則它們在并發(fā)運(yùn)行時(shí)仍然正確,換句話說,數(shù)據(jù)庫防止所有可能的競爭條件。接下來我們將詳細(xì)來聊一聊Serializability的隔離級別是如何實(shí)現(xiàn)的。

兩階段鎖(2PL)

數(shù)據(jù)庫發(fā)展幾十年來,廣泛使用的算法:兩階段鎖(2PL)。

  • 事務(wù)A獲取了數(shù)據(jù)的讀鎖,而事務(wù)B想寫對應(yīng)的數(shù)據(jù),則必須事務(wù)A提交或中止后方可繼續(xù)寫入操作。這可以確保事務(wù)B不會意外地改變事務(wù)A正在讀取的數(shù)據(jù)。
  • 事務(wù)A獲取了數(shù)據(jù)的寫鎖,事務(wù)B想讀取對應(yīng)的數(shù)據(jù),事務(wù)B也必須等到事務(wù)A提交或中止后方可進(jìn)行讀取。
  • 事務(wù)A獲取了數(shù)據(jù)的寫鎖,事務(wù)B想寫對應(yīng)的數(shù)據(jù),事務(wù)B也必須等到事務(wù)A提交或中止后方可進(jìn)行寫入操作。

由上面三個(gè)規(guī)則可以看出,2PL提供串行化的訪問,它可以防止任何的并發(fā)問題,但是由此帶來的問題也顯而易見,數(shù)據(jù)庫的并發(fā)能力大大降低了。

共享鎖與獨(dú)占鎖

兩階段鎖的邏輯是通過共享鎖與獨(dú)占鎖共同來實(shí)現(xiàn)的:
如果事務(wù)A要讀取數(shù)據(jù),則必須先獲取共享鎖。數(shù)據(jù)庫允許多個(gè)事務(wù)同時(shí)擁有共享鎖,但如果另一個(gè)事務(wù)擁有獨(dú)占鎖,則其他事務(wù)要獲取共享鎖則必須等待。

如果事務(wù)A要寫入數(shù)據(jù),則必須先獲取獨(dú)占鎖。任何其他事務(wù)都不能同時(shí)擁有鎖,(無論是共享還是獨(dú)占)因此如果對象上存在任何鎖,事務(wù)A必須等待。

如果事務(wù)A先讀取數(shù)據(jù),然后寫入數(shù)據(jù)。它可以將共享鎖升級為獨(dú)占鎖。升級與直接獲得獨(dú)占鎖相同。

在事務(wù)獲得鎖之后,它必須繼續(xù)持有鎖直到事務(wù)結(jié)束(提交或中止)。這就是“兩階段”的名稱:第一階段在獲取鎖時(shí),第二階段釋放鎖。

由于使用了這么多鎖,所以很容易發(fā)生事務(wù)A被卡住等待事務(wù)B釋放它的鎖,反之亦然。這種情況稱為死鎖。數(shù)據(jù)庫自動檢測死鎖之后會終止事務(wù),然后重啟事務(wù)排隊(duì)。

序列化的快照隔離(SSI)

兩階段鎖(2PL)由于采取了悲觀的并發(fā)控制,不但容易引起死鎖,且性能低下。所以接下來我們要來看看序列化的快照隔離(SSI),它提供了完整的串行化,但是只有很小的性能損失相比兩階段鎖。

當(dāng)我們以前討論快照隔離中的并發(fā)寫問題,是因?yàn)槭聞?wù)從數(shù)據(jù)庫讀取一些數(shù)據(jù),檢查讀取結(jié)果,并決定根據(jù)它看到的結(jié)果采取一些操作。然而,在快照隔離的情況下,原始查詢的結(jié)果在事務(wù)提交時(shí)可能不再是最新的,因?yàn)閿?shù)據(jù)可能在此期間進(jìn)行了修改。所以查詢和事務(wù)中的寫之間可能存在因果依賴關(guān)系。為了提供串行化隔離,數(shù)據(jù)庫可以檢測到這種情況,并且終止不合法的事務(wù)。

檢測是否讀取舊的數(shù)據(jù)

快照隔離通常采用多版本并發(fā)控制實(shí)現(xiàn),當(dāng)一個(gè)事務(wù)讀取一個(gè)數(shù)據(jù)庫的一致性快照,它忽略了新的寫入。為了防止這種異常,數(shù)據(jù)庫需要跟蹤事務(wù)時(shí)讀取時(shí)是否忽略了另一個(gè)事務(wù)的寫操作,當(dāng)事務(wù)要提交時(shí),數(shù)據(jù)庫檢查任何已忽略的寫操作。如果忽略了寫操作,則必須中止事務(wù)。

為什么要等到提交時(shí),而不是檢測到讀取舊數(shù)據(jù)時(shí)就立即終止事務(wù)呢?那么,如果事務(wù)如果是只讀事務(wù),則不需要中止,在事務(wù)進(jìn)行讀取時(shí),數(shù)據(jù)庫還不知道該事務(wù)是否稍后將執(zhí)行寫入操作。上文Alice與Bob請假的例子可以通過這樣的方式避免并發(fā)寫的問題:


檢測到讀取了舊的數(shù)據(jù),事務(wù)終止
檢測影響先前讀取的寫入

如果并沒有檢測到讀取了舊的數(shù)據(jù),仍然有可能出現(xiàn)并發(fā)寫入的問題。

所以當(dāng)事務(wù)寫入數(shù)據(jù)庫時(shí),它記錄讀取受影響數(shù)據(jù)的任何其他事務(wù)的索引。一旦第一個(gè)事務(wù)是成功提交,其他所有相關(guān)的索引事務(wù)必須終止。通過這樣快照隔離的方式,保證了并發(fā)寫入的安全性。同樣是上文的例子,下圖暫時(shí)了索引終止技術(shù):

通過事務(wù)索引終止了被影響數(shù)據(jù)的其他事務(wù)

許多工程細(xì)節(jié)影響算法在實(shí)踐中的工作效果。跟蹤事務(wù)的讀寫的粒度。如果數(shù)據(jù)庫非常詳細(xì)地跟蹤每一個(gè)事務(wù)的活動,那么它就可以精確地判斷哪些事務(wù)需要中止,但是這些開銷會變得很大。而不太詳細(xì)的跟蹤事務(wù)會更快速,但可能導(dǎo)致更多的事務(wù)被中止。相比與兩階段鎖,可串行化隔離快照是大有好處的:一個(gè)事務(wù)不需要阻塞等待另一個(gè)事務(wù)持有的鎖。

小結(jié):

我們在本篇之中總結(jié)了數(shù)據(jù)庫事務(wù)與隔離運(yùn)用到的多種策略與技術(shù),希望大家能夠更好的認(rèn)識事務(wù)在數(shù)據(jù)庫系統(tǒng)之中的重要意義,并且能夠?yàn)樽约旱拈_發(fā)環(huán)境運(yùn)用最恰當(dāng)?shù)母綦x級別。

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

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

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