- 原文地址:Practical Guide to SQL Transaction Isolation
- 原文做者:Joe Nelson
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:sigoden
- 校對者:mnikn, tmpbook
你可能已經在你的數據庫文檔中看到過隔離級別這一個概念,雖然感到有點不安,可是並無太放在心上。一些平常的例子中使用到的事務本質上是隔離。大多數人使用數據庫的的默認隔離級別,並指望獲得最好結果。隔離級別是一個必需要理解的基本概念,並且若是你花點時間學習這個指南,你會以爲生活更愜意。html
我從學術論文中,從 PostgreSQL 文檔中,在與同事就什麼是隔離級別,何時使用它們能在保持應用程序的正確性的同時得到最大運行效率等問題答案的討論中收集了本文須要的信息。前端
爲了正確理解 SQL 隔離級別,咱們須要先思考事務自己。事務的概念來自於以下契約規則:合法交易必須具備原子性(全部條款都同時適用或同時失效),一致性(遵照法律協議),持久性(承諾後各方不能收回承諾)。這些性質就是數據庫管理系統中衆所周知的縮寫詞 ACID 中的 A,C 和 D。最後一個字母 I,意思是隔離,就是本文要重點討論的了。node
在數據庫中而非法律意義中,事務是一組操做,將數據庫從一個一致性狀態轉變到另外一個一致狀態。這意味着,若是全部的數據庫一致性約束條件在執行事務前是知足的,那麼在執行後仍然是知足的。python
數據庫能將這一思想更進一步,在每一條 SQL 數據變動語句中都強加約束嗎?現有的 SQL 命令作不到。它們表達力不足以保證用戶的每一步執行都保持一致性。舉一個經典的例子,將一個銀行賬戶的錢轉移到另外一個帳戶這個過程當中,在咱們將錢從一個帳戶扣除以後,並把錢計入另外一個帳戶以前,存在着一個暫時的不一致狀態。由於這個緣由,事務而不是語句被做爲一致性的基本單位。react
在這一觀念之上,咱們能夠想象事務在數據庫上連續運行着,並一直等待直到輪到它來獨自處理數據的時候。在這個有序的世界中,數據庫將從一個一致的狀態移動到另外一個一致的狀態,中途會短暫地出現的不一致狀態,但並不會形成有害的影響。android
然而,串行事務這麼烏托邦的事情在任何多用戶數據庫系統都幾乎是不可行的。想象一下,一家航空公司的數據庫由於一個用戶預約航班而被鎖定,致使任何人都沒法訪問。ios
值得慶幸的是徹底串行執行事務一般是沒必要要的。許多事務不會對其它事務產生干擾,由於它們更新或讀取的信息被徹底隔離。同時運行此類事務(交錯執行其命令)的最終結果與選擇在另外一個事務以前才運行這個事務沒有什麼區別。這種狀況下的事務,咱們稱之爲可串行化。git
然而,並行執行事務確實有形成衝突的風險。沒有數據庫監督,事務會干擾彼此的工做數據,並運行在不正確的數據庫狀態中。這可能會致使查詢結果不正確和違反約束。github
現代數據庫提供了一些方式自動地選擇性地在一個事務中經過低延時或重試命令來避免干擾。數據庫爲了預防事務間的干涉提供了幾種嚴格程度遞增的模式,被稱做隔離級別。更高等級的隔離級別在檢測和處理衝突上更有效,但也更耗費資源。web
併發事務提供了不一樣等級的隔離級別給開發者,開發者可以平衡併發量和吞吐量,由此來肯定隔離等級。較低的隔離級別會提升事務併發量,但也增長了事務運行在某種不正確數據庫狀態中的風險。
咱們首先要理解哪些併發交互會對應用所需的查詢操做形成威脅,而後才能選擇合適的隔離等級。正如咱們將看到的,有時一個應用程序能夠經過手動操做(如採起顯示鎖定)來下降它常規狀況下須要的隔離級別。
在研究隔離級別以前,讓咱們先停下來看看「動物園」中圈養的事務問題。文獻稱這類問題爲「事務現象」。
對於每一種現象,咱們將深刻探究它併發命令示意圖,分析它爲何有問題,它在何種狀況下能夠被接受,以及它在什麼狀況下是咱們爲達到特定效果有意使用的。
咱們將用一種速記符號表示事務 T1 和 T2 的執行。下面是一些例子:
r1[x]
—— T1 讀取行 x 的值w2[y]
—— T2 寫入行 y 的值c1
—— T1 提交a2
—— T2 中斷事務 T1 修改條目,事務 T2 在事務 T1 提交或回滾前進一步修改。
若是咱們容許髒寫,那麼咱們將不能確保必定能夠回滾事務,想一下這種狀況:
咱們應該回退到狀態 A 嗎?不,由於那樣咱們會失去 w2[x] 。因此咱們應該保持在狀態 C。若是 c2 發生那麼一切就正常了。然而若是 a2 發生了會怎樣?咱們不能回退到狀態 B,由於那樣會丟棄 a1。但咱們不能回退到狀態 C,由於那樣會丟棄 a2。歸謬法能夠論證。
由於髒寫打破了事務的原子性,即便是在最低隔離級別,沒有任何的關係數據庫容許這些操做。經過抽象的方式考慮這個問題,是很具備啓發性的。
髒寫還會破壞一致性。例如,假設約束是 x=y。事務 T1 和 T2 單獨執行都能保證約束,可是它們一塊兒執行將違法約束。
在任何狀況下,髒寫的都是沒有意義的,也不能提供便捷。所以,沒有數據庫容許它們。
一個事務讀取了另外一個未提交的併發事務寫入的數據。(同上面的情景,未提交的數據被視爲「髒」)。
假設 T1 修改行後,T2 讀取了它,接着 T1 回滾了。如今 T2 就持有了「不存在"的一行數據。基於不存在的數據對將來作決策是不正確的。
髒讀,也爲違反約束大開方便之門。假設存在約束 x=y。接着假設 T1 同時將 x 和 y 的值增長 100,T2 同時將值翻倍。任何一個事務單獨執行時都能保證 x=y。但髒讀 w1[x += 100], w2[x *= 2], w2[y *= 2], w1[y += 100] 違反了約束。
最後,即便沒有對併發事務進行回滾,在另外一個事務進行中間操做時啓動的事務也會由於髒讀從而形成數據庫狀態不一致。咱們但願事務啓動時處於一個一致的狀態。
當一個事務須要追蹤另外一個事務時,髒讀是有用的,例如調試和進度監控。也好比,當有一個事務在插入數據時再開一個事務反覆運行 COUNT(*) 以獲取插入速度/進度,但這也僅適用於髒讀不產生危害時。
此外,髒讀這種現象不會發生在對早已再也不變更的歷史信息進行查詢的時候。沒有新的寫入就不會產生問題。
事務讀取它先前已讀取過的數據時,發現它已經被另外一個事務更改了(在初次讀操做以後有發生提交)。
注意,這不一樣於髒讀,由於另外的事務進行過提交。此外這種現象須要兩次讀取才會顯現。
上面的過程涉及到兩個值時稱做不對稱讀:
不可重複讀是一種特殊形式的讀傾斜:b=a
如同髒讀,不可重複讀容許一個事務讀取一個不一致的狀態。它發生的方式稍微不一樣。假設存在約束 x=y。
T1 至始至終沒有讀取任何髒數據,但讀取過程當中 T2 插入,改變一些值,並在 T1 再次讀取前進行了提交。注意這個違規操做甚至不要求 T1 從新讀取相同的值。
不對稱讀可能形成兩個相關元素之間的約束被破壞。例如,假設存在約束 x+y > 0,且:
另外一個涉及到兩個值的違法約束的狀況出如今外鍵和其目標之間。不對稱讀會讓它們混亂。例如,T1 從一個與表 B 相關聯的表 A 中讀取了一行,可是 T2 從表 B 中刪除了該行並進行了提交,這形成表 A 以爲行仍存於表 B 但卻沒法讀取到它。
當備份數據庫的同時運行事務將是災難性的,由於觀察到的狀態可能不一致,將形成沒法執行還原。
非可重複讀容許訪問最新提交的數據。這可能在對大數據(或常常重複數據)進行聚合報告時有用,由於它們能夠容忍讀操做時短暫地違反約束。
事務再次執行返回一組知足搜索條件的行的查詢時,發現知足該條件的行的集合因爲另外一個剛剛提交的事務而發生了更改。
幻讀相似於不可重複讀,但幻讀發生的條件是其匹配查詢條件的集合改變了,而不是單條數據。
有一種狀況是,當一個表包含表明資源分配的行(如僱員和他們的工資)時,其中一個事務做爲「調控者」會增長每行表明的資源,而另外一個事務會插入新行。幻讀會包含新行,使調控者預算超標。
再舉一個相關例子。考慮這樣一個約束:它要求一系列工做任務排單後總時長不能超過 8 小時。T1 讀取了排單,發現總時長只有 7 個小時,因而它添加了一個時長 1 小時的新任務,同時併發事務 T2 也作了一樣的事情。
分頁查詢結果中的新返回頁面包含新添加條目就很合適。一樣,插入或刪除項目後用戶翻頁時商品條目能自動調整。
T1 讀取了一條數據,同時 T2 更新了這條數據。T1 根據讀取的內容也更新了這條數據,而後提交。T2 進行的更新丟失了。
在某些方面,這幾乎感受不到異常。這並不會違反數據庫約束,由於更新丟失只是形成一些工做沒有提交而已。這種狀況與應用程序連續對同一個值進行兩次提交相似。
然而,這畢竟是一個異常,任何其餘事務都沒有機會看到更新,並且 T2 的提交行爲變得像回滾同樣。但一批命令串行執行時有些命令可能觀察到變化,至少它們在檢查值的時候能夠。
在真實世界裏,應用程序在執行讀和寫操做時,丟失更新會形成特別惡劣的影響。
例如,同時有兩人試圖購買某個活動剩下的最後一張入場券,這觸發了兩個事務,事務讀取到還剩下一張未賣出的票。應用程序在單獨線程中生成可打印票據並將其加入郵件隊列,同時修改剩餘票數爲 0。在兩個更新同時完成後,剩餘票數爲 0,這是正確的。然而有一個客戶收到的郵件中的票據是重複的。
最後,請注意,當應用程序(一般經過 ORM)更新行中的全部列,而不只僅是那些自讀取後才更改的列時,丟失更新的風險會增長。
在像 UPDATE foo SET bar = bar + 1 WHERE id = 123;
這樣的原子讀取並更新語句中,更新丟失是不會發生的,由於其它事務不能在 bar 的值讀取和更新之間執行寫操做。這種現象發生在應用程序讀取條目,內部對它進行計算,接着寫入新值的過程當中間。咱們以後會深刻分析。
有時候應用程序在歷史更新中丟失一些值是能夠接受的。傳感器頻繁的覆蓋它經過多線程度量到的值,咱們也只須要讀取它最近記錄的有意義的值。這種狀況下,雖然略有作做,但能夠容忍更新丟失。
兩個併發事務讀取對方正在寫入的數據集來肯定它們寫入的內容。
注意,若是 b=a 那麼上述狀況就變成了更新丟失。
不對稱寫形成事務歷史不可序列化。回想一下,這意味着一個接着一個運行事務獲得的結果沒有辦法與交錯運行時相同。
我見過的最明顯的例子是黑白行。照搬 PostgreSQL 維基文檔:有下面一種狀況,一些行包含一個顏色列,它的值或是「黑」或是「白」。有兩個用戶同時試圖將全部行的顏色變得一致,可是它們嘗試的方向倒是反的。一個用戶試圖將全部行的顏色變爲黑色,另外一個用戶試圖將全部行的顏色變爲白色。
若是這些更新是串行執行的,全部的顏色最終會變得一致。然而若是沒有任何數據庫保護措施,交錯更新將簡單的相互逆轉,留下一堆混合的顏色。
不對稱寫也會打破約束。假設咱們要求 x + y ≥ 0。且
兩個事務都讀到 x 和 y 的值是 100,因此對單個事務來講將某個值變爲負數是能夠的,獲得的和仍然是非負數。然而它們同時將值變爲負致使 x+y=-200,這違反了約束。想要感性的理解的話能夠類比銀行帳戶,銀行帳戶的帳戶收支能夠爲負數,只要總的餘額保持非負數。
事務能夠看到更新了的用來指示批處理已完成的控制記錄,但未看到其中一個記錄着批處理邏輯部分的詳細記錄,由於它讀取的是早期的控制記錄修訂。
前面列舉的異常只須要兩個併發事務就能產生,可是這個須要是三個。它在 2004 年被發現後就一直引人注意,由於它揭示了快照隔離級別(稍後討論)的缺陷,且它是惟一一個在不執行寫入的三個事務的執行中表現出來的異常。
事務競爭進行以下三件事,
歷史證實上述異常不可串行化。順序執行事務帶來不變性,即在生成報告的事務顯示了特定批次的總數以後,後續事務不能更改總數。
數據庫一致性保持無缺,這種異常,僅致使報告的結論是不正確的。
鑑於直到 2004 年纔有人注意到這種現象,它不太可能像其它現象那樣容易引起問題。儘管它在任什麼時候候都不應出現,但它也不是很嚴重。
咱們已經羅列了全部可能出現的事務異常現象嗎?這很難知道;ANSI SQL-92 標準表示它們已經列出了全部異常:髒讀,不可重複讀,幻讀。直到 1995 年,貝倫森等人才發現其餘串行異常,只讀異常直到 2004 年被指出。
第一個關係數據庫使用鎖來管理併發。SQL 標準用事務現象而不是鎖來描述問題,它容許基於非鎖的策略來實現標準。然而,標準做者未能發現其餘異常的緣由是由於他們發現的三個異常都是「假裝的鎖」。
我不知道是否還有更多的沒有列出的事務異常現象,但彷佛頗有可能有。如今有衆多論文在研究可串行性自己的性質,由於它看起來像理論基礎。
商業數據庫經過一系列隔離級別實現併發控制,這些隔離級別其實是受控的反串行。應用程序爲了得到較高的性能一般選擇較低的隔離級別。高的隔離級別意味着更好的事務執行效率和更短的事務平均響應時間。
若是你理解了上一節中「動物園」中的併發問題,那麼你也充分地睿智地理解了如何爲應用程序選擇正確的隔離級別了。這裏須要深刻理解的不是隔離級別如何防止了異常現象,而是隔離級別阻止了什麼異常現象。
在最頂部,串行化時任何異常現象都不會發生。隨着箭頭,阻止標記着的異常發生的保護邏輯被移除。
藍色的三個節點表示的隔離級別被 PostgreSQL 提供。使人費勁的地方是 SQL 規範提供的隔離級別數不足,PostgreSQL 將這些規範中的定義的隔離級別映射到它實際支持的隔離級別。
你須要的 | 你獲得的 |
---|---|
串行化 | 串行化 |
可重複讀 | 快照隔離 |
讀已提交 | 提已提交 |
讀未提交 | 讀已提交 |
例如:
BEGINISOLATIONLEVEL REPEATABLE READ;
-- 如今咱們進入快照隔離
讀已提交是默認的隔離級別,如今想象一下,若是你現有的應用程序沒有采起預防措施,你可能遇到的併發問題。
正如前面所提到的,咱們沒必要深刻了解每個 PostgreSQL 隔離級別能夠防止哪些並發現象,可是咱們須要瞭解兩種通常性方法:樂觀和悲觀併發控制。這是由於每一種方法對應不一樣的應用程序設計技術要求。
悲觀併發控制將對數據庫行進行鎖定,以強制事務等待其執行讀寫操做的時機。由於它老是須要時間來獲取和釋放鎖,沮喪地假設會有衝突,因此叫作「悲觀」。
樂觀控制不會佔用鎖,它只是爲每一個事務生成單獨的數據庫當前狀態快照,觀察可能發生的衝突。若是一個事務干擾了另外一個事務,數據庫將阻止形成干擾的事務並清除它完成的做業。這種方式是有效的,由於干擾其實不多見。
遇到衝突的數量取決於如下幾個因素:
在 PostgreSQL 中,有兩種使用樂觀併發控制的隔離級別:可重複讀(實際就是快照隔離)和可串行化。這些隔離級別並非萬能藥,撒在不安全的應用程序上,就能解決全部的問題。使用它們須要修改應用邏輯。
在構建一個與使用隔離級別由樂觀併發控制的 PostgreSQL 交互的應用程序時必須當心。要知道任何變動在提交前都是不肯定的,全部做業在一瞬時均可能被抹除。應用程序必須時刻準備着,若是檢查到查詢返回錯誤 40001 (表明 serialization_failure
),就要從新執行事務。通用,應用程序在這種事務中不該該執行不可逆的真實操做。應用程序必須使用悲觀鎖來包含這種行爲,或者在收到成功提交的結果後再執行操做。
你可能以爲能夠在一個 PL/pgSQL 函數中緩存串行化異常並執行重試,惋惜重試不能在那兒執行。整個函數運行在一個事務內部, 在調用前就失去了對執行的控制。不幸的的是在提交的時刻發生串行錯誤的可能性最大,而對於函數來講,已經來不及進行捕捉了。
重試必須由數據庫客戶端進行控制。許多語言提供了幫助函數來處理相似任務。這兒列舉了一些。
由於從新生成事務很浪費,因此最好在有限的時間內存儲事務已避免做業丟失。
通常來講,最好使用合適的隔離級別,以免異常和查詢的擾亂。最好讓數據庫作它最擅長的。然而,若是你肯定某些異常不會發生在您的使用場景,你能夠選擇使用一個包含悲觀鎖的低隔離級別。
例如咱們能夠在讀和更新之間添加一個鎖來避免讀提交事務的更新丟失。這隻須要咱們在選擇類語句中添加 "For Update"。
BEGIN;
SELECT *
FROM player
WHERE id = 42
FOR UPDATE;
-- 一些遊戲邏輯
UPDATE player
SET score = 853
WHERE id = 42;
COMMIT;複製代碼
任何試圖選擇一樣的行進行更新的事務都會阻塞,直到第一個事務完成操做爲止。這個使用選擇的更新技巧甚至能夠在串行事務中被用來避免串行錯誤致使的重試,特別是你本打算在應用程序中採起非冪操做時。
最後,你能夠冒着計算不許的風險使用較低的隔離級別。快照隔離級別被採用的一個主要緣由是它比串行有更好的性能,同時還避免了大部分串行化能避免的併發異常。若是在你的場景中不會發生不對稱讀,你能夠下降隔離級別使用快照。
感謝那些爲我寫的這篇文章提供建議的人。
進一步的閱讀
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。