譯者 | 康少京
審校 | 孫淑娟
隔離定義為在數(shù)據(jù)庫并發(fā)執(zhí)行多個事務(wù)時,不會影響到其他事務(wù)的執(zhí)行。本文將解釋這些隔離級別,并概述它們之間的權(quán)衡。我們還建議選擇最適合您需求的隔離級別。
讓我們從有效使用隔離級別所需的最低知識開始,研究表示大多數(shù)應(yīng)用程序的兩個用例及其對不同隔離級別的影響。
用例1:銀行交易
客戶從銀行賬戶取錢:
- 開始交易;
- 讀取用戶余額;
- 在活動表中創(chuàng)建一行(我們避免將其稱為事務(wù),以避免與數(shù)據(jù)庫事務(wù)混淆);
- 從讀取的金額中減去提款金額后,更新用戶的余額;
- 提交。
在交易完成之前,我們不希望用戶的余額發(fā)生變化。
用例2:零售交易
國際客戶從零售店購買物品時使用的貨幣與標(biāo)價不同:
- 開始交易;
- 讀取exchange_rate表,獲取最新的兌換率;
- 在訂單表中創(chuàng)建一行;
- 提交。
假設(shè)有一個單獨的過程正在不斷更新匯率,但我們不關(guān)心匯率在讀取之后是否會發(fā)生變化,即使當(dāng)前交易還沒有完成。
可序列化
Serializable隔離級別是唯一滿足ACID屬性理論定義的級別。它從本質(zhì)上說,兩個并發(fā)事務(wù)不允許相互干擾對方的更改,如果一個接一個地執(zhí)行,則必須產(chǎn)生相同的結(jié)果。
不幸的是,Serializable通常被認(rèn)為是不切實際的,即使對于非分布式數(shù)據(jù)庫。所有現(xiàn)有的流行數(shù)據(jù)庫(如Postgres和MySQL)都不推薦它,這并不是巧合。
為什么這個設(shè)置如此不切實際?讓我們來看看兩個用例:
在銀行用例中,Serializable是完美的。在讀取用戶余額后,數(shù)據(jù)庫保證用戶余額不會改變。因此,應(yīng)用業(yè)務(wù)邏輯是安全的,例如確保用戶有足夠的余額,并根據(jù)讀取的值寫入新的余額。在銀行用例中,Serializable是完美的。
在零售用例中,Serializable也可以正常工作。在創(chuàng)建訂單的事務(wù)成功之前,不允許更新匯率的流程執(zhí)行其操作。
由于事件的精確順序,這聽起來像是一個很棒的功能。但是,如果創(chuàng)建訂單的交易緩慢又復(fù)雜怎么辦?也許它需要去倉庫檢查庫存。也許它必須對下訂單的用戶進行信用檢查。它將持有該行上的鎖,防止匯率進程更新。這種意想不到的依賴關(guān)系可能會阻止系統(tǒng)擴展。
Serializable設(shè)置也會經(jīng)常出現(xiàn)死鎖。例如,如果兩個事務(wù)讀取一個用戶的余額,它們將在該行上放置一個共享讀取鎖。如果事務(wù)稍后修改該行,它們將嘗試將讀鎖升級為寫鎖。這將導(dǎo)致死鎖,因為每個事務(wù)都將被另一個事務(wù)持有的讀鎖阻塞。正如我們將在下面看到的,不同的隔離級別可以很容易地避免這個問題。
換句話說,有爭議的工作負(fù)載將無法使用Serializable設(shè)置進行擴展。如果工作負(fù)載沒有爭議,我們就不需要這個隔離級別。較低的隔離可能同樣有效。
為了解決這種不必要且昂貴的安全問題,必須重構(gòu)應(yīng)用程序。例如,獲取匯率的代碼在事務(wù)開始之前調(diào)用,或者使用單獨的連接來完成讀取程序。
雖然理論上沒有那么純粹,但其他隔離級別允許您在個案的基礎(chǔ)上執(zhí)行序列化讀取。這使得它們在編寫可伸縮系統(tǒng)時更加靈活和實用。
無鎖定實現(xiàn)
有一些方法可以在不鎖定數(shù)據(jù)的情況下提供可序列化的一致性。然而,這類系統(tǒng)也會遇到上述相同的問題,即沖突交易的失敗方式不同。問題的根本原因在于隔離級別本身,任何實現(xiàn)都無法讓您擺脫這些約束。
重復(fù)讀
RepeatableRead是一個模糊的設(shè)置。因為它區(qū)分了點選擇和搜索,并為每個點定義了不同的行為。這不是非黑即白的,并導(dǎo)致了許多其他實現(xiàn)。這里就不詳細(xì)討論這個隔離級別。然而,就我們的用例而言,RepeatableRead提供了與Serializable相同的保證,因此繼承了相同的問題。
快照讀
SnapshotRead隔離級別雖然不是ANSI標(biāo)準(zhǔn),但已經(jīng)越來越流行了。也被稱為MVCC。這種隔離級別的優(yōu)點是無爭用:它在事務(wù)開始時創(chuàng)建一個快照。所有讀取都發(fā)送到該快照,而不獲取任何鎖。但寫操作遵循嚴(yán)格的可序列化規(guī)則。
SnapshotRead事務(wù)對于只讀工作負(fù)載最有價值,因為您可以看到一致的數(shù)據(jù)庫快照。這避免了在加載事務(wù)上相互依賴的不同數(shù)據(jù)片段時出現(xiàn)意外。還可以使用快照功能在特定時間讀取多個表,然后觀察自該快照以來發(fā)生的更改。對于希望將更改流式傳輸?shù)椒治鰯?shù)據(jù)庫的更改數(shù)據(jù)捕獲工具,這個功能非常方便。
對于執(zhí)行寫入的事務(wù),快照特性不是很有用。您主要想控制是否允許在上次讀取后更改值。如果您想允許該值更改,它將在您閱讀后立即失效,因為其他人可以稍后對其進行更新。因此,無論您是從快照讀取還是獲取最新值,這都沒有關(guān)系。如果不希望更改,則需要最新的值,并且必須鎖定行以防止更改。
換句話說,SnapshotRead對于只讀工作負(fù)載很有用,但對于寫工作負(fù)載來說,它并不比ReadCommitted好,我們將在下面介紹。
在此隔離級別中重新應(yīng)用Retail用例可以很自然地工作,不會產(chǎn)生爭用:從匯率中讀取的值產(chǎn)生了創(chuàng)建事務(wù)時快照的值。在進行此交易時,允許單獨的交易來更新匯率。
銀行用例如何?數(shù)據(jù)庫允許您對數(shù)據(jù)進行鎖定。例如,MySQL能夠“在共享模式下選擇…鎖定”(讀鎖)。此模式將讀取升級為可序列化事務(wù)的讀取。當(dāng)然,還繼承了此隔離級別的死鎖風(fēng)險。
較低的隔離級別可以兩全其美。您可以發(fā)出一個“select…for update”(寫鎖)。此鎖阻止另一個事務(wù)獲取此行上的任何類型的鎖。這種悲觀鎖定方法一開始聽起來很糟糕,但它允許兩個競爭事務(wù)成功完成,而不會遇到死鎖。第二個事務(wù)將等待第一個事務(wù)完成,此時它將讀取并鎖定新值所在的行。
MySQL默認(rèn)支持SnapshotRead隔離級別,但會將其稱為REPEATABLE_READ。
分布式數(shù)據(jù)庫
雖然單個數(shù)據(jù)庫有多種有效實現(xiàn)可重復(fù)讀取的方法,但在分布式數(shù)據(jù)庫中,問題變得更加復(fù)雜。這是因為事務(wù)可以跨越多個碎片。如果是這樣,系統(tǒng)必須提供嚴(yán)格的訂購保證。這種排序要求系統(tǒng)使用集中的并發(fā)控制機制或全局一致的時鐘。這兩種方法本質(zhì)上都試圖將原本可以彼此獨立執(zhí)行的事件緊密耦合起來。
因此,在希望分布式數(shù)據(jù)庫支持分布式快照讀取之前,必須了解并愿意接受這些權(quán)衡。
已提交
ReadCommitted隔離比SnapshotRead更明確,因為它不斷返回數(shù)據(jù)庫的最新視圖。這也是隔離級別中爭議最小的。在這個級別上,每次讀取一行時可能會得到不同的值。
ReadCommitted設(shè)置還允許您通過發(fā)出讀或?qū)戞i定來升級讀取,從而有效地允許您按需執(zhí)行可序列化讀取。正如前面所說的,對于打算修改數(shù)據(jù)的應(yīng)用程序事務(wù),這種方法提供了兩全其美的解決方案。
Postgres支持的默認(rèn)隔離級別是ReadCommitted。
讀取未提交
這種隔離級別通常被認(rèn)為是不安全的,不建議用于分布式或非分布式設(shè)置。這是因為您可能會讀取稍后可能回滾的數(shù)據(jù)(或者從一開始就不存在的數(shù)據(jù))。
分布式事務(wù)
這個主題與隔離級別是正交的,但這里必須涵蓋這一點,因為它在保持事物的松散耦合方面具有重要意義。
在分布式系統(tǒng)中,如果兩行位于不同的碎片或數(shù)據(jù)庫中,并且您希望在單個事務(wù)中原子化地修改它們,則會產(chǎn)生兩階段提交(2PC)的開銷。
這需要更多的工作:
- 創(chuàng)建關(guān)于分布式事務(wù)的元數(shù)據(jù)并保存到持久存儲中。
- 對所有單個交易發(fā)布準(zhǔn)備。
- 提交的決策保存到元數(shù)據(jù)中。
- 向準(zhǔn)備好的事務(wù)發(fā)出提交。
prepare要求您保存元數(shù)據(jù),以便在提交(或回滾)前,如果節(jié)點發(fā)生崩潰,可以在新的leader中恢復(fù)事務(wù)。
分布式事務(wù)還與隔離級別交互。例如,假設(shè)只有2PC事務(wù)的第一次提交成功,第二次提交被延遲。如果應(yīng)用程序已經(jīng)讀取了第一次提交的效果,那么數(shù)據(jù)庫必須阻止應(yīng)用程序讀取第二次提交的行,直到完成。反過來說,如果應(yīng)用程序在第二次提交之前讀取了一行,那么它肯定看不到第一次提交的效果。
數(shù)據(jù)庫必須做額外的工作來支持分布式事務(wù)的隔離保證。如果應(yīng)用程序可以容忍這些部分提交呢?然后,我們就做了應(yīng)用程序不關(guān)心的不必要的工作??赡苤档靡胍粋€新的隔離級別,如ReadPartialCommits。請注意,這不同于ReadUncommitted,用戶讀取的數(shù)據(jù)最終可能被回滾。
最后,過度使用2PC會降低系統(tǒng)的整體可用性和延遲。這是因為性能最差的碎片將決定您的有效可用性。
總結(jié)
為了具有可伸縮性,應(yīng)用程序應(yīng)該避免依賴數(shù)據(jù)庫的任何高級隔離功能。相反,它應(yīng)該盡可能少地使用擔(dān)保。如果可以編寫一個應(yīng)用程序來使用ReadCommitted隔離級別,那么不建議遷移到SnapshotRead。Serializable或RepeatableRead。
最好避免多語句事務(wù),但隨著應(yīng)用程序的發(fā)展,這可能會不可避免。此時,嘗試主要依賴事務(wù)的原子保證,并保持?jǐn)?shù)據(jù)庫系統(tǒng)支持的最低隔離級別。
如果使用分片數(shù)據(jù)庫,請完全避免分布式事務(wù)。這可以通過將相關(guān)行保留在同一個碎片中來實現(xiàn)。必須從一開始就這樣做,因為很難將非并發(fā)程序重構(gòu)為并發(fā)程序。
譯者介紹
康少京,51CTO社區(qū)編輯,從事通訊類行業(yè),底層驅(qū)動開發(fā)崗位。
原文標(biāo)題:??Optimizing Isolation Levels for Scaling Distributed Databases??,作者:Sugu Sougoumarane