資料庫的事務和併發問題
資料庫事務
事務(Transaction)是併發控制的基本單位。所謂的事務,它是一個操作序列,這些操作要麼全部執行,要麼全部都不執行。比如,銀行轉賬,從一個賬號扣錢,然後另一個賬號餘額增加,這兩個操作要麼都執行,要麼都不執行。這兩個操作組合在一起就是事務。
資料庫事務有嚴格的定義,它必須同時滿足4個特性:
- 原子性,Atomic
- 一致性,Consistency
- 隔離性, Isolation
- 永續性,Durabiliy
簡稱ACDI。下面是對每一個特性的說明:
- 原子性:表示組成一個事務的多個數據庫操作是一個密不可分的原子單元,只有所有的操作執行成功,整個事務才提交。事務中的任何一個數據庫操作失敗,已經執行的任何操作都必須撤銷(回滾),讓資料庫恢復到事務提交之前的狀態。
- 一致性:資料庫總是從一個一致性狀態裝換到另一個一致性狀態。一致性狀態的含義是資料庫中的資料應該滿足資料庫約束。
- 隔離性:在併發資料操作時,不同的事務擁有各自的資料空間,它們的操作不會對對方產生干擾。但是也並非要做到完全無干擾。資料庫規定了多個隔離級別,不同的隔離級別的干擾程度是不同,隔離級別越高,資料一致性越好,但併發性越弱。
- 永續性:一旦資料庫提交之後,事務中的所有操作都必須被持久化都資料庫中。即使在提交事務後,資料庫重啟時,也必須保證能夠通過某種機制恢復資料。
在這些事務的特徵中,資料”一致性“是最終目標,其他特性都是為達到這個目標而採取的措施。
資料庫併發的問題
一個數據庫可能會有多個客戶端同時訪問,資料庫中相同的資料就有可能同時被多個事務訪問,如果沒有采取必要的隔離措施,就會導致各種問題,破壞資料的完整性,這些問題可以分為5中,兩類:
- 資料讀取的問題:
- 髒讀
- 不可重複讀
- 幻想讀
- 資料更新問題
- 第一類丟失更新
- 第二類丟失更新
1. 髒讀(direct read)
A事務讀取B事務尚未提交更改的資料,並在這個資料的基礎上進行操作。如果恰巧B事務回滾,那麼A事務讀取到的資料是不被承認的。通過一個取款事務和轉賬事務來說明這個問題。
時間 | 轉賬事務A | 取款事務B |
---|---|---|
T1 | 開始事務 | |
T2 | 開始事務 | 查詢賬戶餘額1000元 |
T3 | 取出500元,把餘額改為500元 | |
T4 | 查詢餘額500元(髒讀) | |
T5 | 撤銷事務,餘額恢復為1000元 | |
T6 | 匯入100元,把餘額改為600元 | |
T8 | 提交事務 |
在這個場景中轉賬事務A讀取到取款事務B中的未提交的資料,導致髒讀。
2. 不可重複讀(unrepeatable read)
不可重複讀是指A事務讀取B事務已經提交更改的資料。假設A在取款事務的過程中,B往該賬戶轉賬100元,A兩次讀取賬戶的餘額不一致。
時間 | 取款事務A | 轉賬事務B |
---|---|---|
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 查詢賬戶餘額為1000元 | |
T4 | 查詢賬戶餘額1000元 | |
T5 | 取出100元,把餘額改為900元 | |
T6 | 查詢事務 | |
T7 | 查詢賬戶餘額為900元 |
在同一事務中,T4和T7時間點讀取的賬戶餘額不一致。
3. 幻想讀(phantom read)
A事務讀取B事務提交的新增資料,這時A事務將出現幻想讀現象。幻想讀一般發生在計算統計資料的事務中。
舉個例子,比如在銀行系統的同一個事務中有兩次統計存款使用者的總金額,在兩次統計中剛好新增了一個存款,這時,兩次統計的結構將會不一致。
時間 | 統計金額事務A | 轉賬事務B |
---|---|---|
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 統計總存款為1000元 | |
T4 | 新增一個存款賬戶,存款100元 | |
55 | 提交事務 | |
T6 | 再次統計總存款數為10100元(幻想讀) |
如果新增的資料剛好滿足事務的查詢條件,那麼這個新資料就會出現事務的視野中,因而產生了兩次結構不一致。
幻想讀和不可重複讀這兩個概念比較容易混淆 ,幻想讀是指讀到了其他已經提交的事務的新增資料,而不可重複讀是指讀到了已經提交的更改資料(更改或者刪除)。
為了避免這兩種情況,採取的對策是不同的:防止讀到更新的資料,只需要對操作的資料新增行級鎖,阻止操作中的資料發生改變;而防止讀到新增的資料,則往往需要新增表級鎖——將整張表加鎖,防止新增資料。
4. 第一類丟失更新
第一類丟失更新是一個事務撤銷時把另一個事務的資料覆蓋了。下面通過一個轉賬的例子來看一下這個問題。
時間 | 取款事務A | 轉賬事務B |
---|---|---|
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 查詢餘額為1000元 | |
T4 | 查詢餘額為1000元 | |
T5 | 匯入100元,把餘額修改為1100元 | |
T6 | 提交事務 | |
T7 | 取出100元,把餘額修改為900元 | |
T8 | 撤銷修改 | |
T9 | 把餘額恢復為1000元(丟失更新) |
5. 第二類丟失更新
一個事務覆蓋另一個事務已經提交的資料。造成另一個事務所做的操作丟失。
時間 | 取款事務A | 轉賬事務B |
---|---|---|
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 查詢賬戶餘額為1000元 | |
T4 | 查詢賬戶餘額為1000元 | |
T5 | 取出100元,把餘額修改為900元 | |
T6 | 提交事務 | |
T7 | 匯入100元 | |
T8 | 提交事務 | |
T9 | 把餘額修改為1100元(丟失更新) |
總結:第一類為撤銷時覆蓋,第二類為提交時覆蓋。
資料庫鎖機制
資料庫的併發會引起很多問題,當然有些問題還可以容忍,但是有的問題卻是致命的。併發問題一般都會用鎖解決,在資料庫中也是用鎖解決的,但是不同的資料庫對於鎖的實現是不同的,但基本的原理是相同。
按鎖定的物件可以分為:
- 表鎖定:對於整張表鎖定
- 行鎖定:對於表中的特定行鎖定
從併發的資料關係中又可以分為
- 獨佔鎖:共享鎖會防止獨佔鎖,但允許其他共享鎖的訪問。
- 共享鎖:獨佔鎖獨自佔領表或行,防止其他共享鎖的訪問,當然也訪問其他獨佔鎖。
在資料更新的時候,資料庫必須在進行更改的行上施加行獨佔鎖,也就是說INSERT,UPDATE,DELETE等語句都會隱式採用必要的行鎖定。
事務的隔離級別
儘管資料庫為使用者提供了鎖的DML操作方式,但是直接使用還是很麻煩的,因此資料庫為使用者提供了自動鎖的機制。也就是隔離級別,只要使用者指定的回話的隔離級別,資料庫就會分析SQL語句,然後進行合適的加鎖,當資料鎖的資料太多的時候,自動進行鎖升級來提高系統的,效能,這一過程對使用者是透明的(不可見)的。
SQL標準定義了4個事務級別,每一個級別都規定了一個事務中所做的修改,哪些在事務中是可見的,哪些是不可見的。較低的隔離通常可以執行更高的併發,系統開銷也更低。
下面的是四中資料庫事務的介紹:
- READ UNCOMMITED(未提交讀) 事務中的修改,即使沒有提交對其它事務都是可見的。事務可以讀取未提交的資料,這也被稱為髒讀。一般很少使用。
- READ COMMITED(提交讀) 大多數的資料庫的預設隔離級別都是READ COMMITED。READ_COMMITED從一個事務開始時,只能”看見“已經提交的修改。也就是說:一個事務從開始到提交前,所做的任何修改對其他事務是不可見的。這個級別有時候也叫做不可重複讀,因為兩次執行查詢可能會得到不同的結果。
- REPEATABLE READ(可重複讀) REPEATABLE READ解決了髒讀的問題,該級別保證在同一個事務中多次讀取同樣的記錄的結果是一致的。但是這個級別還是沒有解決另一個問題:幻讀。
- SERIALIZABLE(可序列化) SERIALIZABLE是最高的隔離級別。它通過事務串執行,避免了前面所說的幻讀問題。簡單來說SERIALIZABLE會為每一行資料都加鎖,所以會導致大量的鎖超時和競爭。實際中很少使用這個隔離級別。
下表為事務隔離級別對併發問題的解決情況:
隔離級別 | 髒讀 | 不可重複讀 | 幻想讀 | 第一類丟失更新 | 第二類丟失更新 |
---|---|---|---|---|---|
READ UNCOMMITED | 允許 | 允許 | 允許 | 不允許 | 允許 |
READ COMMITED | 不允許 | 允許 | 允許 | 不允許 | 允許 |
REPEATABLE READ | 不允許 | 不允許 | 允許 | 不允許 | 不允許 |
SERIALIZABLE | 不允許 | 不允許 | 不允許 | 不允許 | 不允許 |
其中READ UNCOMMITED併發性和吞吐量最好,SERIALIZABLE的最差,所以事務的隔離級別和資料庫的併發行是對立的。