資料庫事務隔離級別的深坑:預設值應修改為SERIALIZABLE

banq發表於2019-05-04

本文提出將資料庫的預設級別修改為可序列化SERIALIZABLE,不用擔心效能降低,他們發現在一個設計良好的系統中,SERIALIZABLE和READ COMMITTED之間的效能差異可以忽略不計!

幾十年來,資料庫系統為使用者提供了多種隔離級別可供選擇,範圍從高階的“可序列化”到低端的“讀取提交”或“未提交讀取”。這些不同的隔離級別將帶來應用程式不同型別的併發錯誤。儘管如此,許多資料庫使用者仍然堅持使用他們資料庫系統的預設隔離級別,並且不必考慮哪個隔離級別對於他們的應用程式是最佳的,這種做法很危險!

絕大多數廣泛使用的資料庫系統 - 包括Oracle,IBM DB2,Microsoft SQL Server,SAP HANA,MySQL和PostgreSQL--預設情況下不保證任何可序列化的保證。我們將在下面詳細介紹,隔離級別弱於可序列化化可能導致應用程式中的併發錯誤和負面使用者體驗,因此資料庫使用者瞭解資料庫系統保證的隔離級別以及可能出現的併發錯誤非常重要。

許多資料庫使用者堅持使用預設的隔離級別......並且不考慮哪種隔離級別對於他們的應用程式是最佳的

在這篇文章中,我們提供了有關資料庫隔離級別的教程,較低隔離級別帶來的優勢,以及這些較低階別可能允許的併發錯誤型別。我們在這篇文章中的主要關注點是可序列化隔離級別與將應用程式暴露給特定型別導致的併發異常,以及較低階別之間的區別。在可序列化隔離的類別中也存在重要差異(例如,“嚴格可序列化”產生與“單拷貝可序列化”是不同的一組保證)。

什麼是“隔離級別”?

資料庫隔離是指資料庫允許事務執行的能力,就好像沒有其他併發執行的事務一樣(即使實際上可能存在大量併發執行的事務)。總體目標是防止對併發事務寫入產生的臨時,中止或其他不正確資料進行讀寫操作。

確實有完美隔離這樣的東西(我們將在下面定義)。不幸的是,完美性通常會付出效能成本:產生延遲(事務完成之前多長時間)或吞吐量(系統完成每秒事務數)下降。

根據特定系統的架構方式,完美隔離變得更容易或更難實現。在設計不良的系統中,實現完美性會帶來令人望而卻步的效能成本,並且這些系統的使用者將被推動接受明顯缺乏完美性的保證。

即使在設計良好的系統中,通過接受不完美的保證通常也可以獲得非平凡的效能優勢。因此,隔離級別的出現:它們為系統使用者提供了在效能與資料正確性之間的權衡能力。

完美的隔離

讓我們通過提供“完美”隔離的概念開始討論資料庫隔離級別。我們將上面的隔離定義為資料庫系統允許事務執行的能力,就好像沒有其他併發執行的事務一樣(即使實際上可能是大量併發執行的事務)。在這方面完美是什麼意思?乍一看,似乎完美是不可能的。如果兩個事務都讀取和寫入相同的資料項,那麼它們相互影響是至關重要的。如果他們互相忽略,那麼無論哪個事務完成,寫入最後都會破壞第一個事務,從而產生相同的最終狀態,就好像它從未執行過一樣。

資料庫系統是最早的可擴充套件併發系統之一,並且已成為隨後開發的許多其他併發系統的原型。幾十年前,資料庫系統社群開發了一種非常強大(但可能未被充分認識)的機制來處理實現併發程式的複雜性。

這個想法如下:人類在推理併發方面從根本上是不好的。編寫一個無錯誤的非併發程式是很困難的。但是一旦你新增了併發性,就會出現一個接近無限的競爭條件。幾乎不可能不考慮不同執行緒中的程式執行可能彼此重疊的所有不同方式,以及不同型別的重疊如何導致不同的最終狀態。

相反,資料庫系統為應用程式開發人員提供了一個漂亮的抽象 - 一個“事務”。事務可能包含任意程式碼,但它基本上是單執行緒的。

應用程式開發人員只需要關注事務中的程式碼 - 以確保在系統中沒有其他併發程式執行時它是正確的。給定資料庫的任何起始狀態,程式碼不得違反應用程式的語義。確保程式碼的正確性並非易事,但是當程式碼單獨執行時,確保程式碼的正確性要比確保程式碼在與可能嘗試讀取或寫入共享資料的其他程式碼一起執行時更正確更容易。 

如果應用程式開發人員能夠在沒有其他併發程式執行時確保其程式碼的正確性,那麼保證完美隔離的系統將確保程式碼保持正確,即使系統中可能同時執行的其他程式碼可能會讀取或寫入相同的資料。

實現這種完美程度聽起來很困難,但實際上它實際上相當簡單。我們已經假設在沒有任何啟動狀態的併發執行時程式碼是正確的。因此,如果事務程式碼是序列執行的 - 一個接一個地執行(序列) - 那麼最終狀態也將是正確的。

因此,為了實現完美的隔離,所有系統必須做的是確保當事務同時執行時,最終狀態等同於系統狀態,如果它們是序列執行的,那麼它將會實現最終狀態等同於系統狀態。

有幾種方法可以實現這一點 - 例如通過鎖定,驗證或多版本化,我們的目的的關鍵點是我們將“完美隔離”定義為系統並行執行事務的能力,但其方式相當於它們一個接一個地執行。在SQL標準中,這種完美的隔離級別稱為可序列化。

分散式系統中的隔離級別變得更加複雜。許多分散式系統實現了可序列化隔離級別的變體,例如一個拷貝可序列化(1SR), 嚴格可序列化(嚴格1SR)或更新可序列化(US)。

其中,  “嚴格可序列化”是這些可序列​​​​​​​化選項中最完美的選擇。

併發系統中的異常​​​​​​​

SQL標準定義了幾個低於可序列化的隔離級別。此外,商業資料庫系統中常見的其他隔離級別 - 最明顯的是快照隔離 - 不包含在SQL標準中。

在我們討論這些不同的隔離級別之前,讓我們討論一些眾所周知的應用程式錯誤/異常情況,這些錯誤可能發生在低於可序列化的隔離級別。我們將使用零售示例來描述這些錯誤。

假設每當客戶購買一個小部件時,就會執行以下“購買”交易:

  1. 讀取舊庫存
  2. 寫入新庫存,比第1步中讀取的庫存要少一個
  3. 將與此次購買相對應的新訂單插入到訂單表中

如果此類訂單交易連續執行,則將始終考慮所有初始庫存。如果有42個小部件,那麼在任何時候,剩餘的所有庫存和訂單表中的訂單的總和應該是42。(banq注:等同於DDD的聚合不變性)

但是,如果這個事務以低於可序列化的隔離級別同時執行呢?

例如,假設兩個併發執行的事務讀取相同的初始庫存(42),然後兩者都嘗試寫入除了新訂單之外的一個小於他們讀取的值(41)的新庫存。

在這種情況下,最終狀態是41的庫存,但訂單表中有兩個新訂單(總共有43個小部件被佔用)。我們創造了一個無中生有的小部件!顯然,這是一個錯誤。它被稱為丟失更新異常。 

作為另一個例子,假設這兩個相同的事務同時執行,但這次第二個事務在第一個事務的步驟(2)和(3)之間開始。在這種情況下,第二個事務在減少後讀取庫存的值 - 即它讀取值41並將其減少到40,並寫入訂單。與此同時,第一筆交易在寫入訂單時中止(例如由於信用卡下降)。

在這種情況下,在中止過程中,第一個事務在它開始之前(當庫存為42時)恢復到資料庫的狀態。因此,最終狀態是42的庫存,並且寫入一個訂單(來自第二個交易)。這一次,我們又創造了一個無中生有的小部件!這被稱為髒寫異常 (因為第二個事務在決定是否提交或中止之前覆蓋了第一個事務的寫入值)。

作為第三個例子,假設一個單獨的事務執行庫存和訂單表的讀取,以便對所有存在的小部件進行計算。如果它在購買事務的步驟(2)和(3)之間執行,它將看到資料庫的臨時狀態,其中視窗小部件已從清單中消失,但尚未作為訂單出現。看起來小部件已經丟失 - 我們的應用程式中的另一個錯誤。這被稱為髒讀異常,因為允許事務讀取購買交易的臨時(不完整)狀態。

作為第四個例子,假設一個單獨的事務檢查庫存並在剩下少於10個小部件的情況下獲取更多小部件:

  1. IF (READ(Inventory) = (10 OR 11 OR 12))運送一些新的小部件以通過標準運輸補充庫存
  2. IF (READ(Inventory) < 10)通過快遞運送一些新的小部件來補充庫存

請注意,此事務會兩次讀取庫存。如果購買交易在此交易的步驟(1)和(3)之間執行,則每次將讀取不同的庫存值。如果在購買交易之前的初始庫存是10,這將導致相同的重新進貨請求進行兩次 - 標準運輸一次和快遞一次。這稱為不可重複的讀異常

作為第五個例子,假設一個事務掃描訂單表以計算訂單的最高價格,然後再次掃描以查詢平均訂單價格。在這兩次掃描之間,插入了一個非常昂貴的訂單,使得平均值偏差太大,以至於它變得高於前一次掃描中找到的最高價格。此交易返回的平均價格大於最高價格 - 顯然是不可能的,並且在可序列化系統中永遠不會發生錯誤。這個錯誤與不可重複的讀取異常略有不同,因為事務讀取的每個值在兩次掃描之間保持不變 - 錯誤的來源是在這兩次掃描之間插入了額外的記錄。這被稱為幻像讀異常

作為最後一個示例,假設應用程式允許小部件的價格根據庫存進行更改。例如,隨著航班庫存的減少,許多航空公司提高了機票價格。假設應用程式使用公式來約束這兩個變數如何相互關聯 - 例如I + P> = $ 500(其中I是庫存而P是價格)。在允許購買成功之前,購買事務必須檢查庫存和價格,以確保不違反上述約束。如果不違反約束,則可以繼續通過該購買事務更新庫存。

同樣,實施特殊促銷折扣的單獨交易可以檢查庫存和價格,以確保在作為促銷的一部分更新價格時不違反約束。如果不違反,價格可以更新。

現在,假設這兩個交易同時執行 - 它們都讀取了I和P的舊值,並獨立地決定它們的庫存和價格更新分別不會違反約束。因此,他們繼續進行更新。不幸的是,這可能會導致I和P的新值違反約束!如果一個在另一個之前執行,第一個將成功,另一個將在第一個完成後讀取I和P的值,並檢測到它們的更新將違反約束,因此不會繼續。但由於它們同時執行,它們都會看到舊值並錯誤地決定它們可以繼續進行更新。這個錯誤稱為寫偏斜異常 因為它發生在兩個事務讀取相同資料但更新已讀取資料的不相交子集時。

ISO SQL標準中的定義

SQL標準根據哪些異常可能來定義降低的隔離級別。特別是,它包含下表:

隔離級別                           髒讀     不可重複讀   幻影讀
READ UNCOMMITTED                     可能     可能        可能
READ COMMITTED                       不可能    可能        可能   
重複讀                             不可能   不可能       可能
SERIALIZABLE序列化                     不可能   不可能      不可能

SQL標準如何定義這些隔離級別有很多很多問題。大多數這些問題已經在1995年指出,但是莫名其妙地,修改SQL標準之後的修訂已經從那時開始釋出而沒有解決這些問題。

第一個問題 是標準只使用三種型別的異常來定義每個隔離級別 - 髒讀,不可重複讀和幻讀。但是,有許多型別的併發錯誤還是能在實踐中出現。

 

第二個問題是,使用異常來定義隔離級別只會讓終端使用者能夠確保哪些特定型別的併發錯誤是不可能的。它沒有給出任何特定事務可見的潛在資料庫狀態的精確定義。在學術文獻中給出了幾種改進的和更精確的隔離水平定義。Atul Adya的博士論文基於如何交錯來自不同事務的讀寫,給出了SQL標準隔離級別的精確定義。然而,從系統的角度給出了這些定義。在最近由娜塔莎Crooks等工作。從使用者的角度來看,al給出了優雅和精確的定義。

第三個問題是標準沒有定義,也沒有對實際使用的最流行的降低隔離級別之一提供正確性約束:快照隔離(也沒有任何多種變體 - PSINMSIRead Atomic等)。由於未能提供快照隔離的定義,因此跨系統出現了快照隔離所允許的併發漏洞的差異。通常,快照隔離執行從包含僅提交資料的資料庫狀態的特定快照開始的所有資料讀取。此快照在事務的整個生命週期內保持不變,因此保證所有讀取都是可重複的(除了僅提交資料)。此外,寫入相同資料的併發事務檢測到彼此衝突,並且通常通過中止其中一個衝突事務來解決此衝突。這可以防止丟失更新異常。但是,只有在衝突的事務寫入重疊的資料集時才會檢測到衝突。如果寫集是不相交的,這些衝突將無法被發現。因此,快照隔離容易受到寫入偏斜異常的影響。某些實現也容易受到幻像讀取異常的影響。

第四個問題是SQL標準似乎給出了序列化SERIALIZABLE隔離級別的兩個不同定義。首先,它正確定義了SERIALIZABLE:最終結果必須等同於沒有併發時可能發生的結果。但是,它提供了上面的表,這似乎意味著只要隔離級別就能夠實現不允許髒讀,不可重複讀或幻讀,它就可以稱為序列化SERIALIZABLE?

Oracle歷來利用這種模糊性來證明其實現快照隔離“SERIALIZABLE”的合理性。說實話,我認為大多數讀過ISO  SQL標準的人 我們會相信前面給出的更準確的SERIALIZABLE定義(這是正確的)是作者的意圖。

儘管如此,我想甲骨文的律師已經對它進行了研究,並確定該檔案中有足夠的模糊性來合法地證明他們依賴其他定義。

最重要的是:幾乎不可能給出應用程式開發人員可用的實際隔離級別的明確定義,因為SQL標準中的模糊性和模糊性導致了跨實現/系統的語義差異。 

你應該選擇什麼樣的隔離級別?

對應用程式設計師的建議如下:降低隔離級別是危險的!很難弄清楚哪些併發錯誤可能會出現。

如果每個系統使用Crooks等的方法定義它們的隔離級別,至少你會對其相關保證有一個精確和正式的定義。遺憾的是,對於大多數資料庫使用者來說,Crooks論文的形式主義過於先進,因此資料庫供應商不太可能很快在他們的文件中採用這些形式。

降低隔離水平是危險的......降低隔離水平的定義在實踐中仍然含糊不清,使用風險很大

正確的選擇通常是避免比可序列化隔離更低的隔離級別!對於絕大多數資料庫系統,您實際上必須更改預設值才能完成此任務!

但是,有三個警告:

  1. 正如我上面提到的,一些系統使用“SERIALIZABLE”這個詞來表示比真正的可序列化隔離更弱的東西。不幸的是,這意味著只需在資料庫系統中選擇表面上的“SERIALIZABLE隔離級別”可能在實際中不能確保可序列化。您需要檢查文件以確保它以下列方式定義SERIALIZABLE:資料庫的可見狀態始終等效於在沒有併發時可能發生的狀態。否則,您的應用程式可能容易受到寫入偏斜異常的影響。
  2. 如上所述,可序列化隔離級別帶來效能成本。根據系統架構的質量,可序列化的效能成本可能很大或很小。在我最近與Jose Faleiro和Joe Hellerstein一起撰寫的一篇研究論文中,我們發現在一個設計良好的系統中,SERIALIZABLE和READ COMMITTED之間的效能差異可以忽略不計......在某些情況下,SERIALIZABLE隔離級別可能是(令人驚訝的是)優於READ COMMITTED隔離級別。如果您發現系統中可序列化隔離的成本過高,那麼您應該考慮使用不同於您考慮降低隔離級別的資料庫系統。
  3. 在分散式系統中,即使在可序列化的隔離級別中,也可能(並且確實)出現重要的異常。對於這樣的系統,重要的是要理解可序列化隔離類的元素之間的細微差別(已知嚴格的可序列化是最安全的)。我們將在以後的文章中闡述這個問題。

相關文章