1. 程式人生 > >事務與隔離級別------《Designing Data-Intensive Applications》讀書筆記10

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

串行化 clas block atomic 硬件故障 nsis 特性 筆記 額外

和數據庫打交道的程序員繞不開的話題就是:事務,作為一個簡化訪問數據庫的應用程序的編程模型。通過使用事務,應用程序可以忽略某些潛在的錯誤場景和並發問題,由數據庫負責處理它們。而並非每個應用程序都需要事務,有時削弱事務性擔保或完全放棄事務,可以獲得更高的性能或更高的可用性。怎麽樣更好的理解數據庫中的事務與隔離級別呢?我們借這篇文章來聊一聊吧~

1.ACID

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

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

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

  • 隔離性(Isolation):
    數據庫由多個客戶端同時訪問時,如果他們訪問相同的數據庫記錄,你會遇到並發問題。如下圖所示:
    技術分享圖片
    隔離性意味著並發執行的事務彼此隔離,數據庫確保當事務提交時,結果與它們順序運行相同,即使它們實際上是並發運行的。

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

在幾種特性之中,隔離性是DBA對數據庫調優最為側重的部分,接下來,我們著重來聊一聊事務的隔離性。

2. 隔離級別

如果兩個事務不觸及相同的數據,它們可以安全地並行運行,因為兩者都不依賴於其他數據。當一個事務讀取另一個事務同時修改的數據,或者兩個事務試圖同時修改同一數據時,便會出現並發問題。

並發錯誤很難通過測試發現,因為這種的錯誤觸發具有偶然性,通常很難重現。並發性也很難推理,尤其是在大型應用程序中,因為開發人員不一定知道其他代碼片段正在訪問數據庫。所以數據庫通過提供事務的隔離性來隱藏應用程序開發者的並發問題,屏蔽了底層數據庫的並發細節,提供了一個串行化的數據模型。

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

Read Committed

最基本的隔離級別是Read Committed

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

一個事務已經向數據庫寫入了一些數據,但該事務尚未提交或中止。另一個事務可以看到未提交的數據,就稱為臟讀Read Committed的隔離級別可以防止臟讀。所以當事務提交之後,事務中的寫操作才對其他人可見。如下圖所示:
技術分享圖片

臟寫:

寫操作覆蓋了一個未提交的值,被稱之為臟寫Read Committed的隔離級別事務可以防止臟寫,通常是通過延遲寫操作直到前一個寫事務已提交或中止時在繼續寫入。臟寫會導致數據出現不一致,如下圖所示:Alice和Bob要買同一個東西,臟寫導致了最終的買家是Bob,而發票卻寄給了Alice。
技術分享圖片

實現:

Read Committed是一種十分流行的隔離級別,許多數據庫的默認隔離級別便是Read Committed。

數據庫通過使用行級鎖防止臟寫:當事務要修改某個特定行時,它必須首先獲取該行的鎖。然後必須保留該鎖,直到事務提交或中止為止。只有一個事務可以鎖定任何給定行的鎖;如果另一個事務要寫入同一個行,則必須等到第一個事務提交或中止後才可獲取鎖並繼續。

而使用行級鎖避免臟讀會產生很大的代價,容易找出讀延遲。使用當事務正在進行時,讀取同一行的任何其他事務都只給出舊值。只有當新值被提交時,事務才切換到讀取新值。

Read Repeatable

Read Committed看起來是一個很好的隔離級別了,但是它也會產生一些問題,我們看下面這個例子:如圖所示,Alice在一家銀行有1000美元的存款,在兩個賬戶上拆分,每個賬戶有500美元。現在,一個事務從她的帳戶轉到另一個帳戶100美元。如果她很不幸地在事務正在進行的同一時刻查看她的賬戶余額清單,她可能會看到一個賬戶余額在收到的款項到達之前(余額為500美元),另一個賬戶在已進行的轉移之後(新余額為400美元),而100美元消失了。
技術分享圖片

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

快照隔離

為了實現可重復讀,我們需要快照隔離的技術。

每個事務都從數據庫的快照中讀取的,即事務在事務開始時看到數據庫中提交的所有數據。即使數據隨後被另一個事務更改,每個事務只看到來自特定時間點的舊數據。當事務可以看到數據庫的數據,在特定時間點被凍結了。

快照隔離的實現通常使用寫鎖來防止臟寫,這意味著編寫的事務可以阻止寫入同一對象的另一個事務的進程。實現快照隔離,數據庫必須保留數據的幾個不同的提交版本,因為各種正在進行的事務可能需要在不同的時間點查看數據庫的狀態,這種技術被稱為多版本並發控制(MVCC)

如下圖所示,每當一個事務向數據庫寫入任何內容時,它寫入的數據都會用事務ID進行標記。
技術分享圖片

當事務從數據庫中讀取時,事務ID用於決定哪些數據可見,哪些數據是不可見的。在每次更改值時創建新版本,數據庫可以提供快照隔離,而只產生較小的開銷。

Serializability

Read Repeatable雖然解決了讀取數據的問題,但是依然沒有辦法解決並發寫的問題。我們來看看下面這個例子:醫院通常在任何時候都要有幾個值班醫生,必須至少有一位醫生在值班。醫生可以調整他們的輪班,前提是至少有一個同事在醫院值班。Alice和Bob是兩位今天值班的醫生。兩人都想調整輪班,不幸的是,他們碰巧點擊按鈕大約在同一時間取消輪班。接下來發生的情況如圖所示:
技術分享圖片

由於數據庫的隔離級別是快照隔離,兩個人都檢查到目前有兩個人值班,因此兩個事務都進入下一個階段。Alice認為請假沒有問題,Bob也認為請假沒有問題。兩個事務都提交了,現在沒有醫生在值班了,數據庫的一致性出現了問題。

Serializability 被看作是最強的隔離級別。數據庫保證,如果事務在單獨運行時行為正確,則它們在並發運行時仍然正確,換句話說,數據庫防止所有可能的競爭條件。接下來我們將詳細來聊一聊Serializability的隔離級別是如何實現的。

兩階段鎖(2PL)

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

  • 事務A獲取了數據的讀鎖,而事務B想寫對應的數據,則必須事務A提交或中止後方可繼續寫入操作。這可以確保事務B不會意外地改變事務A正在讀取的數據。
  • 事務A獲取了數據的寫鎖,事務B想讀取對應的數據,事務B也必須等到事務A提交或中止後方可進行讀取。
  • 事務A獲取了數據的寫鎖,事務B想寫對應的數據,事務B也必須等到事務A提交或中止後方可進行寫入操作。

由上面三個規則可以看出,2PL提供串行化的訪問,它可以防止任何的並發問題,但是由此帶來的問題也顯而易見,數據庫的並發能力大大降低了。

共享鎖與獨占鎖

兩階段鎖的邏輯是通過共享鎖與獨占鎖共同來實現的:
如果事務A要讀取數據,則必須先獲取共享鎖。數據庫允許多個事務同時擁有共享鎖,但如果另一個事務擁有獨占鎖,則其他事務要獲取共享鎖則必須等待。

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

如果事務A先讀取數據,然後寫入數據。它可以將共享鎖升級為獨占鎖。升級與直接獲得獨占鎖相同。

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

由於使用了這麽多鎖,所以很容易發生事務A被卡住等待事務B釋放它的鎖,反之亦然。這種情況稱為死鎖。數據庫自動檢測死鎖之後會終止事務,然後重啟事務排隊。

序列化的快照隔離(SSI)

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

當我們以前討論快照隔離中的並發寫問題,是因為事務從數據庫讀取一些數據,檢查讀取結果,並決定根據它看到的結果采取一些操作。然而,在快照隔離的情況下,原始查詢的結果在事務提交時可能不再是最新的,因為數據可能在此期間進行了修改。所以查詢和事務中的寫之間可能存在因果依賴關系。為了提供串行化隔離,數據庫可以檢測到這種情況,並且終止不合法的事務。

檢測是否讀取舊的數據

快照隔離通常采用多版本並發控制實現,當一個事務讀取一個數據庫的一致性快照,它忽略了新的寫入。為了防止這種異常,數據庫需要跟蹤事務時讀取時是否忽略了另一個事務的寫操作,當事務要提交時,數據庫檢查任何已忽略的寫操作。如果忽略了寫操作,則必須中止事務。

為什麽要等到提交時,而不是檢測到讀取舊數據時就立即終止事務呢?那麽,如果事務如果是只讀事務,則不需要中止,在事務進行讀取時,數據庫還不知道該事務是否稍後將執行寫入操作。上文Alice與Bob請假的例子可以通過這樣的方式避免並發寫的問題:
技術分享圖片

檢測影響先前讀取的寫入

如果並沒有檢測到讀取了舊的數據,仍然有可能出現並發寫入的問題。

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

技術分享圖片

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

小結:

我們在本篇之中總結了數據庫事務與隔離運用到的多種策略與技術,希望大家能夠更好的認識事務在數據庫系統之中的重要意義,並且能夠為自己的開發環境運用最恰當的隔離級別。

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