數據并發的問題
一個數據庫可能擁有多個訪問客戶端,這些客戶端都可以并發方式訪問數據庫。數據庫中的相同數據可能同時被多個事務訪問,如果沒有采取必要的隔離措施,就會導致各種并發問題,破壞數據的完整性。這些問題可以歸結為5類,包括3類數據讀問題(臟讀、幻象讀和不可重復讀)以及2類數據更新問題(第一類丟失更新和第二類丟失更新)。
臟讀(dirty read)
事務讀取B事務尚未提交的更改數據,并在這個數據的基礎上操作。如果恰巧B事務回滾,那么A事務讀到的數據根本是不被承認的。來看取款事務和轉賬事務并發時引發的臟讀場景
時間 |
轉賬事務A |
取款事務B |
T1 |
開始事務 |
|
T2 |
開始事務 |
|
T3 |
|
查詢賬戶余額為1000元 |
T4 |
|
取出500元把余額改為500元 |
T5 |
查詢賬戶余額為500元(臟讀) |
|
T6 |
撤銷事務余額恢復為1000元 |
|
T7 |
匯入100元把余額改為600元 |
|
T8 |
提交事務 |
B希望取款500元而后又撤銷了動作,而A往相同的賬戶中轉賬100元,就因為A事務讀取了B事務尚未提交的數據,因而造成賬戶白白丟失了500元。
不可重復讀(unrepeatable read)
不可重復讀是指A事務讀取了B事務已經提交的更改數據。假設A在取款事務的過程中,B往該賬戶轉賬100元,A兩次讀取賬戶的余額發生不一致:
時間 |
取款事務A |
轉賬事務B |
T1 |
開始事務 |
|
T2 |
開始事務 |
|
T3 |
|
查詢賬戶余額為1000元 |
T4 |
查詢賬戶余額為1000元 |
|
T5 |
|
取出100元把余額改為900元 |
T6 |
提交事務 |
|
T7 |
查詢賬戶余額為900元(和T4讀取的不一致) |
在同一事務中,T4時間點和T7時間點讀取賬戶存款余額不一樣。
幻象讀(phantom read)
A事務讀取B事務提交的新增數據,這時A事務將出現幻象讀的問題。幻象讀一般發生在計算統計數據的事務中,舉一個例子,假設銀行系統在同一個事務中,兩次統計存款賬戶的總金額,在兩次統計過程中,剛好新增了一個存款賬戶,并存入100元,這時,兩次統計的總金額將不一致:
時間 |
統計金額事務A |
轉賬事務B |
T1 |
開始事務 |
|
T2 |
開始事務 |
|
T3 |
統計總存款數為10000元 |
|
T4 |
新增一個存款賬戶,存款為100元 |
|
T5 |
提交事務 |
|
T6 |
再次統計總存款數為10100元(幻象讀) |
如果新增數據剛好滿足事務的查詢條件,這個新數據就進入了事務的視野,因而產生了兩個統計不一致的情況。
幻象讀和不可重復讀是兩個容易混淆的概念,前者是指讀到了其它已經提交事務的新增數據,而后者是指讀到了已經提交事務的更改數據(更改或刪除),為了避免這兩種情況,采取的對策是不同的,防止讀取到更改數據,只需要對操作的數據添加行級鎖,阻止操作中的數據發生變化,而防止讀取到新增數據,則往往需要添加表級鎖——將整個表鎖定,防止新增數據(Oracle使用多版本數據的方式實現)。
第一類丟失更新
A事務撤銷時,把已經提交的B事務的更新數據覆蓋了。這種錯誤可能造成很嚴重的問題,通過下面的賬戶取款轉賬就可以看出來:
時間 |
取款事務A |
轉賬事務B |
T1 |
開始事務 |
|
T2 |
開始事務 |
|
T3 |
查詢賬戶余額為1000元 |
|
T4 |
查詢賬戶余額為1000元 |
|
T5 |
匯入100元把余額改為1100元 |
|
T6 |
提交事務 |
|
T7 |
取出100元把余額改為900元 |
|
T8 |
撤銷事務 |
|
T9 |
余額恢復為1000元(丟失更新) |
A事務在撤銷時,“不小心”將B事務已經轉入賬戶的金額給抹去了。
第二類丟失更新
A事務覆蓋B事務已經提交的數據,造成B事務所做操作丟失:
時間 |
轉賬事務A |
取款事務B |
T1 |
|
開始事務 |
T2 |
開始事務 |
|
T3 |
|
查詢賬戶余額為1000元 |
T4 |
查詢賬戶余額為1000元 |
|
T5 |
取出100元把余額改為900元 |
|
T6 |
提交事務 |
|
T7 |
匯入100元 |
|
T8 |
提交事務 |
|
T9 |
把余額改為1100元(丟失更新) |
上面的例子里由于支票轉賬事務覆蓋了取款事務對存款余額所做的更新,導致銀行最后損失了100元,相反如果轉賬事務先提交,那么用戶賬戶將損失100元。
數據庫鎖機制
數據并發會引發很多問題,在一些場合下有些問題是允許的,但在另外一些場合下可能卻是致命的。數據庫通過鎖的機制解決并發訪問的問題,雖然不同的數據庫在實現細節上存在差別,但原理基本上是一樣的。
按鎖定的對象的不同,一般可以分為表鎖定和行鎖定,前者對整個表進行鎖定,而后者對表中特定行進行鎖定。從并發事務鎖定的關系上看,可以分為共享鎖定和獨占鎖定。共享鎖定會防止獨占鎖定,但允許其它的共享鎖定。而獨占鎖定既防止其它的獨占鎖定,也防止其它的共享鎖定。為了更改數據,數據庫必須在進行更改的行上施加行獨占鎖定,INSERT、UPDATE、DELETE和SELECT FOR UPDATE語句都會隱式采用必要的行鎖定。下面我們介紹一下ORACLE數據庫常用的5種鎖定:
? 行共享鎖定:一般通過SELECT FOR UPDATE語句隱式獲得行共享鎖定,在Oracle中你也可以通過LOCK TABLE IN ROW SHARE MODE語句顯式獲得行共享鎖定。行共享鎖定并不防止對數據行進行更改的操作,但是可以防止其它會話獲取獨占性數據表鎖定。允許進行多個并發的行共享和行獨占性鎖定,還允許進行數據表的共享或者采用共享行獨占鎖定;
? 行獨占鎖定:通過一條INSERT、UPDATE或DELETE語句隱式獲取,或者通過一條LOCK TABLE IN ROW EXCLUSIVE MODE語句顯式獲取。這個鎖定可以防止其它會話獲取一個共享鎖定、共享行獨占鎖定或獨占鎖定;
? 表共享鎖定:通過LOCK TABLE IN SHARE MODE語句顯式獲得。這種鎖定可以防止其它會話獲取行獨占鎖定(INSERT、UPDATE或DELETE),或者防止其它表共享行獨占鎖定或表獨占鎖定,它允許在表中擁有多個行共享和表共享鎖定。該鎖定可以讓會話具有對表事務級一致性訪問,因為其它會話在你提交或者回溯該事務并釋放對該表的鎖定之前不能更改這個被鎖定的表;
? 表共享行獨占:通過LOCK TABLE IN SHARE ROW EXCLUSIVE MODE語句顯式獲得。這種鎖定可以防止其它會話獲取一個表共享、行獨占或者表獨占鎖定,它允許其它行共享鎖定。這種鎖定類似于表共享鎖定,只是一次只能對一個表放置一個表共享行獨占鎖定。如果A會話擁有該鎖定,則B會話可以執行SELECT FOR UPDATE操作,但如果B會話試圖更新選擇的行,則需要等待;
? 表獨占:通過LOCK TABLE IN EXCLUSIVE MODE顯式獲得。這個鎖定防止其它會話對該表的任何其它鎖定。
事務隔離級別
盡管數據庫為用戶提供了鎖的DML操作方式,但直接使用鎖管理是非常麻煩的,因此數據庫為用戶提供了自動鎖機制。只要用戶指定會話的事務隔離級別,數據庫就會分析事務中的SQL語句,然后自動為事務操作的數據資源添加上適合的鎖。此外數據庫還會維護這些鎖,當一個資源上的鎖數目太多時,自動進行鎖升級以提高系統的運行性能,而這一過程對用戶來說完全是透明的。
ANSI/ISO SQL 92標準定義了4個等級的事務隔離級別,在相同數據環境下,使用相同的輸入,執行相同的工作,根據不同的隔離級別,可以導致不同的結果。不同事務隔離級別能夠解決的數據并發問題的能力是不同的。
表 1 事務隔離級別對并發問題的解決情況
隔離級別 |
臟讀 |
不可 重復讀 |
幻象讀 |
第一類丟失更新 |
第二類丟失更新 |
READ UNCOMMITED |
允許 |
允許 |
允許 |
不允許 |
允許 |
READ COMMITTED |
不允許 |
允許 |
允許 |
不允許 |
允許 |
REPEATABLE READ |
不允許 |
不允許 |
允許 |
不允許 |
不允許 |
SERIALIZABLE |
不允許 |
不允許 |
不允許 |
不允許 |
不允許 |
事務的隔離級別和數據庫并發性是對立的,兩者此增彼長。一般來說,使用READ UNCOMMITED隔離級別的數據庫擁有最高的并發性和吞吐量,而使用SERIALIZABLE隔離級別的數據庫并發性最低。
SQL 92定義READ UNCOMMITED主要是為了提供非阻塞讀的能力,Oracle雖然也支持READ UNCOMMITED,但它不支持臟讀,因為Oracle使用多版本機制徹底解決了在非阻塞讀時讀到臟數據的問題并保證讀的一致性,所以,Oracle的READ COMMITTED隔離級別就已經滿足了SQL 92標準的REPEATABLE READ隔離級別。
SQL 92推薦使用REPEATABLE READ以保證數據的讀一致性,不過用戶可以根據應用的需要選擇適合的隔離等級。