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