數(shù)據(jù)庫(kù)(和其他的事務(wù)系統(tǒng))試圖確保事務(wù)隔離性 (transaction isolation),這意味著,從每個(gè)并發(fā)事務(wù)的觀點(diǎn)來(lái)看,似乎沒(méi)有其他的事務(wù)在運(yùn)行。傳統(tǒng)上而言,這已經(jīng)通過(guò)鎖(locking)實(shí)現(xiàn)了。事務(wù)可以在 數(shù)據(jù)庫(kù)中一個(gè)特定的數(shù)據(jù)項(xiàng)目上放置一把鎖,暫時(shí)防止通過(guò)其他事務(wù)訪問(wèn)這個(gè)項(xiàng)目。一些現(xiàn)代的數(shù)據(jù)庫(kù)(如Oracle和PostgreSQL)通過(guò)多版本并發(fā) 控制(multiversion concurrency control,MVCC)實(shí)現(xiàn)事務(wù)隔離性,這種多版本并發(fā)控制通常被認(rèn)為是更可伸縮的。我們將討論假設(shè)了鎖模型的隔離性;但是我們的大部分論述也適用于 多版本并發(fā)控制。
數(shù)據(jù)庫(kù)如何實(shí)現(xiàn)并發(fā)控制,這在Hibernate和 Java Persistence應(yīng)用程序中是至關(guān)重要的。應(yīng)用程序繼承由數(shù)據(jù)庫(kù)管理系統(tǒng)提供的隔離性保證。例如,Hibernate從不鎖上內(nèi)存中的任何東西。如 果你認(rèn)為數(shù)據(jù)庫(kù)供應(yīng)商有多年實(shí)現(xiàn)并發(fā)控制的經(jīng)驗(yàn),就會(huì)發(fā)現(xiàn)這種方法的好處。另一方面,Hibernate和Java Persistence中的一些特性(要么因?yàn)槟闶褂盟鼈?,要么按照設(shè)計(jì))可以改進(jìn)遠(yuǎn)甚于數(shù)據(jù)庫(kù)所提供的隔離性保證。
以幾個(gè)步驟討論并發(fā)控制。我們探討最底層,研究數(shù)據(jù)庫(kù)提供的事務(wù)隔離性保證。然后,看一下Hibernate和Java Persistence特性對(duì)于應(yīng)用程序級(jí)的悲觀和樂(lè)觀并發(fā)控制,以及Hibernate可以提供哪些其他的隔離性保證。
10.2.1? 理解數(shù)據(jù)庫(kù)級(jí)并發(fā)
作為Hibernate應(yīng)用程序的開(kāi)發(fā)人員,你的任 務(wù)是理解數(shù)據(jù)庫(kù)的能力,以及如果特定的場(chǎng)景(或者按照數(shù)據(jù)完整性需求)需要,如何改變數(shù)據(jù)庫(kù)的隔離性行為。讓我們退一步來(lái)看。如果我們正在討論隔離性,你 可能假設(shè)兩種情況,即隔離或者不隔離;現(xiàn)實(shí)世界中沒(méi)有灰色區(qū)域。說(shuō)到數(shù)據(jù)庫(kù)事務(wù),完全隔離性的代價(jià)就很高了。有幾個(gè)隔離性級(jí)別(isolation level),它們一般削弱完全的隔離性,但提升了系統(tǒng)的性能和可伸縮性。
1.事務(wù)隔離性問(wèn)題
首先,來(lái)看一下削弱完全事務(wù)隔離性時(shí)可能出現(xiàn)的幾種現(xiàn)象。ANSI SQL標(biāo)準(zhǔn)根據(jù)數(shù)據(jù)庫(kù)管理系統(tǒng)允許的現(xiàn)象定義了標(biāo)準(zhǔn)的事務(wù)隔離性級(jí)別:
如果兩個(gè)事務(wù)都更新一個(gè)行,然后第二個(gè)事務(wù)異常終止,就會(huì)發(fā)生丟失更新(lost update),導(dǎo)致兩處變化都丟失。這發(fā)生在沒(méi)有實(shí)現(xiàn)鎖的系統(tǒng)中。此時(shí)沒(méi)有隔離并發(fā)事務(wù)。如圖10-2所示。
?
|
|
|
|
|
|
|
|
|
|
|
|
圖10-2 丟失更新:兩個(gè)事務(wù)更新沒(méi)有加鎖的同一數(shù)據(jù)
如果一個(gè)事務(wù)讀取由另一個(gè)還沒(méi)有被提交的事務(wù)進(jìn)行的改變,就發(fā)生臟讀?。╠irty read)。這很危險(xiǎn),因?yàn)橛善渌聞?wù)進(jìn)行的改變隨后可能回滾,并且第一個(gè)事務(wù)可能編寫無(wú)效的數(shù)據(jù),如圖10-3所示。
圖10-3 臟讀?。菏聞?wù)A讀取沒(méi)有被提交的數(shù)據(jù)
如果一個(gè)事務(wù)讀取一個(gè)行兩次,并且每次讀取不同的狀態(tài),就會(huì)發(fā)生不可重復(fù)讀取(unrepeatable read)。例如,另一個(gè)事務(wù)可能已經(jīng)寫到這個(gè)行,并已在兩次讀取之間提交,如圖10-4所示。
圖10-4 不可重復(fù)讀取:事務(wù)A執(zhí)行不可重復(fù)讀取兩次
不可重復(fù)讀取的一個(gè)特殊案例是二次丟失更新問(wèn)題(second lost updates problem)。想象兩個(gè)并發(fā)事務(wù)都讀取一個(gè)行:一個(gè)寫到行并提交,然后第二個(gè)也寫到行并提交。由第一個(gè)事務(wù)所做的改變丟失了。如果考慮需要幾個(gè)數(shù)據(jù)庫(kù)事務(wù)來(lái)完成的應(yīng)用程序?qū)υ?,這個(gè)問(wèn)題就特別值得關(guān)注。我們將在稍后更深入地探討這種情況。
幻讀(phantom read)發(fā)生在一個(gè)事務(wù)執(zhí)行一個(gè)查詢兩次,并且第二個(gè)結(jié)果集包括第一個(gè)結(jié)果集中不可見(jiàn)的行,或者包括已經(jīng)刪除的行時(shí)。(不需要是完全相同的查詢。)這種情形是由另一個(gè)事務(wù)在兩次查詢執(zhí)行之間插入或者刪除行造成的,如圖10-5所示。
?
|
|
|
|
圖10-5 幻讀:事務(wù)A在第二次選擇中讀取新數(shù)據(jù)
既然已經(jīng)了解所有可能發(fā)生的不好的情況,我們就可以定義事務(wù)隔離性級(jí)別了,并看看它們阻止了哪些問(wèn)題。
2.ANSI事務(wù)隔離性級(jí)別
標(biāo)準(zhǔn)的隔離性級(jí)別由ANSI SQL標(biāo)準(zhǔn)定義,但是它們不是SQL數(shù)據(jù)庫(kù)特有的。JTA也定義了完全相同的隔離性級(jí)別,稍后你將用這些級(jí)別聲明想要的事務(wù)隔離性。隔離性級(jí)別的增加帶來(lái)了更高成本以及嚴(yán)重的性能退化和可伸縮性:
l??? 允許臟讀取但不允許丟失更新的系統(tǒng),據(jù)說(shuō)要在讀取未提交(read uncommitted)的隔離性中操作。如果一個(gè)未提交事務(wù)已經(jīng)寫到一個(gè)行,另一個(gè)事務(wù)就不可能再寫到這個(gè)行。但任何事務(wù)都可以讀取任何行。這個(gè)隔離性級(jí)別可以在數(shù)據(jù)庫(kù)管理系統(tǒng)中通過(guò)專門的寫鎖來(lái)實(shí)現(xiàn)。
l??? 允許不可重復(fù)讀取但不允許臟讀取的系統(tǒng),據(jù)說(shuō)要實(shí)現(xiàn)讀取提交(read committed)的事務(wù)隔離性。這可以用共享的讀鎖和專門的寫鎖來(lái)實(shí)現(xiàn)。讀取事務(wù)不會(huì)阻塞其他事務(wù)訪問(wèn)行。但是未提交的寫事務(wù)阻塞了所有其他的事務(wù)訪問(wèn)該行。
l??? 在可重復(fù)讀?。╮epeatable read)隔離性模式中操作的系統(tǒng)既不允許不可重復(fù)讀取,也不允許臟讀取?;米x可能發(fā)生。讀取事務(wù)阻塞寫事務(wù)(但不阻塞其他的讀取事務(wù)),并且寫事務(wù)阻塞所有其他的事務(wù)。
l??? 可序列化(serializable)提供最嚴(yán)格的事務(wù)隔離性。這個(gè)隔離性級(jí)別模擬連續(xù)的事務(wù)執(zhí)行,好像事務(wù)是連續(xù)地一個(gè)接一個(gè)地執(zhí)行,而不是并發(fā)地執(zhí)行。序列化不可能只用低級(jí)鎖實(shí)現(xiàn)。一定有一些其他的機(jī)制,防止新插入的行變成對(duì)于已經(jīng)執(zhí)行會(huì)返回行的查詢的事務(wù)可見(jiàn)。
鎖系統(tǒng)在DBMS中具體如何實(shí)現(xiàn)很不相同;每個(gè)供應(yīng)商都有不同的策略。你應(yīng)該查閱DBMS文檔,找出更多有關(guān)鎖系統(tǒng)的信息,如何逐步加強(qiáng)鎖(例如從低級(jí)別到頁(yè)面,到整張表),以及每個(gè)隔離性級(jí)別對(duì)于系統(tǒng)性能和可伸縮性有什么影響。
知道所有這些技術(shù)術(shù)語(yǔ)如何定義,這很好,但是它如何幫助你給應(yīng)用程序選擇隔離性級(jí)別呢?
3.選擇隔離性級(jí)別
開(kāi)發(fā)人員(包括我們自己)經(jīng)常不確定要在一個(gè)產(chǎn)品應(yīng)用程序中使用哪種事務(wù)隔離性級(jí)別。隔離性太強(qiáng)會(huì)損害高并發(fā)應(yīng)用程序的可伸縮性。隔離性不足則可能在應(yīng)用程序中導(dǎo)致費(fèi)解的、不可重現(xiàn)的bug,直到系統(tǒng)過(guò)載運(yùn)行時(shí)才會(huì)發(fā)現(xiàn)。
注意,我們?cè)诮酉聛?lái)的闡述中所指的樂(lè)觀鎖 (optimistic locking)(利用版本),是本章稍后要解釋的一個(gè)概念。你可能想要跳過(guò)這一節(jié),并且當(dāng)要在應(yīng)用程序中決定隔離性級(jí)別時(shí)再回來(lái)。畢竟,選擇正確的隔離 性級(jí)別很大程度上取決于特定的場(chǎng)景。把以下討論當(dāng)作建議來(lái)讀,不要把它們當(dāng)作金科玉律。
在數(shù)據(jù)庫(kù)的事務(wù)語(yǔ)義方面,Hibernate努力盡可能地透明。不過(guò),高速緩存和樂(lè)觀鎖影響著這些語(yǔ)義。在Hibernate應(yīng)用程序中要選擇什么有意義的數(shù)據(jù)庫(kù)隔離性級(jí)別呢?
首先,消除讀取未提交隔離性級(jí)別。在不同的事務(wù)中使 用一個(gè)未提交的事務(wù)變化是很危險(xiǎn)的。一個(gè)事務(wù)的回滾或者失敗將影響其他的并發(fā)事務(wù)。第一個(gè)事務(wù)的回滾可能戰(zhàn)勝其他的事務(wù),或者甚至可能導(dǎo)致它們使數(shù)據(jù)庫(kù)處 于一種錯(cuò)誤的狀態(tài)中。甚至由一個(gè)終止回滾的事務(wù)所做的改變也可能在任何地方被提交,因?yàn)樗鼈兛梢宰x取,然后由另一個(gè)成功的事務(wù)傳播!
其次,大多數(shù)應(yīng)用程序不需要可序列化隔離性(幻讀通常不成問(wèn)題),并且這個(gè)隔離性級(jí)別往往難以伸縮?,F(xiàn)有的應(yīng)用程序很少在產(chǎn)品中使用序列化隔離性,但在某些情況下,有效地強(qiáng)制一個(gè)操作序列化地執(zhí)行相當(dāng)依賴于悲觀鎖(請(qǐng)見(jiàn)接下來(lái)的幾節(jié)內(nèi)容)。
這樣就把選擇讀取提交還是可重復(fù)讀取留給你來(lái)決定 了。我們先考慮可重復(fù)讀取。如果所有的數(shù)據(jù)訪問(wèn)都在單個(gè)原子的數(shù)據(jù)庫(kù)事務(wù)中執(zhí)行,這個(gè)隔離性級(jí)別就消除了一個(gè)事務(wù)可能覆蓋由另一個(gè)并發(fā)事務(wù)所做變化(第二 個(gè)丟失更新問(wèn)題)的可能性。事務(wù)持有的讀鎖防止了并發(fā)事務(wù)可能希望獲得的任何寫鎖。這是一個(gè)重要的問(wèn)題,但是啟用可重復(fù)讀取并不是唯一的解決辦法。
假設(shè)你正使用版本化(versioned)的數(shù)據(jù), 這是Hibernate可以自動(dòng)完成的東西。(必需的)持久化上下文高速緩存和版本控制的組合已經(jīng)提供了可重復(fù)讀取隔離性的大部分優(yōu)良特性。特別是,版本 控制防止了二次丟失更新問(wèn)題,并且持久化上下文高速緩存也確保了由一個(gè)事務(wù)加載的持久化實(shí)例狀態(tài)與由其他事務(wù)所做的變化隔離開(kāi)來(lái)。因此,如果你使用版本化 的數(shù)據(jù),那么對(duì)于所有數(shù)據(jù)庫(kù)事務(wù)來(lái)說(shuō),讀取提交的隔離性是可以接受的。
可重復(fù)讀取給查詢結(jié)果集(只針對(duì)數(shù)據(jù)庫(kù)事務(wù)的持續(xù)期間)提供了更多的可復(fù)制性;但是因?yàn)榛米x仍然可能,這似乎沒(méi)有多大價(jià)值。可以在Hibernate中給一個(gè)特定的事務(wù)和數(shù)據(jù)塊顯式地獲得可重復(fù)讀取的保證(通過(guò)悲觀鎖)。
設(shè)置事務(wù)隔離性級(jí)別允許你給所有的數(shù)據(jù)庫(kù)事務(wù)選擇一個(gè)好的默認(rèn)鎖策略。如何設(shè)置隔離性級(jí)別呢?
4.設(shè)置隔離性級(jí)別
與數(shù)據(jù)庫(kù)的每一個(gè)JDBC連接都處于DBMS的默認(rèn)隔離性級(jí)別——通常是讀取提交或者可重復(fù)讀取。可以在DBMS配置中改變這個(gè)默認(rèn)。還可以在應(yīng)用程序端給JDBC連接設(shè)置事務(wù)隔離性,通過(guò)一個(gè)Hibernate配置選項(xiàng):
Hibernate在啟動(dòng)事務(wù)之前,給每一個(gè)從連接池中獲得的JDBC連接設(shè)置這個(gè)隔離性級(jí)別。對(duì)于這個(gè)選項(xiàng)有意義的值如下(你也可能發(fā)現(xiàn)它們?yōu)閖ava.sql.Connection中的常量):
l??? 1——讀取未提交隔離性。
l??? 2——讀取提交隔離性。
l??? 3——可重復(fù)讀取隔離性。
l??? 4——可序列化隔離性。
注意,Hibernate永遠(yuǎn)不會(huì)改變?cè)谕泄墉h(huán)境中從應(yīng)用程序服務(wù)器提供的數(shù)據(jù)庫(kù)連接中獲得的連接隔離性級(jí)別!可以利用應(yīng)用程序服務(wù)器的配置改變默認(rèn)的隔離性級(jí)別。(如果使用獨(dú)立的JTA實(shí)現(xiàn)也一樣。)
如你所見(jiàn),設(shè)置隔離性級(jí)別是影響所有連接和事務(wù)的一個(gè)全局選項(xiàng)。給特定的事務(wù)指定一個(gè)更加限制的鎖經(jīng)常很有用。Hibernate和Java Persistence依賴樂(lè)觀的并發(fā)控制,并且兩者都允許你通過(guò)版本檢查和悲觀鎖,獲得額外的鎖保證。
10.2.2? 樂(lè)觀并發(fā)控制
樂(lè)觀的方法始終假設(shè)一切都會(huì)很好,并且很少有沖突的 數(shù)據(jù)修改。在編寫數(shù)據(jù)時(shí),樂(lè)觀并發(fā)控制只在工作單元結(jié)束時(shí)才出現(xiàn)錯(cuò)誤。多用戶的應(yīng)用程序通常默認(rèn)為使用讀取提交隔離性級(jí)別的樂(lè)觀并發(fā)控制和數(shù)據(jù)庫(kù)連接。只 有適當(dāng)?shù)臅r(shí)候(例如,當(dāng)需要可重復(fù)讀取的時(shí)候)才獲得額外的隔離性保證;這種方法保證了最佳的性能和可伸縮性。
1.理解樂(lè)觀策略
為了理解樂(lè)觀并發(fā)控制,想象兩個(gè)事務(wù)從數(shù)據(jù)庫(kù)中讀取 一個(gè)特定的對(duì)象,并且兩者都對(duì)它進(jìn)行修改。由于數(shù)據(jù)庫(kù)連接的讀取提交隔離性級(jí)別,因此沒(méi)有任何一個(gè)事務(wù)會(huì)遇到任何臟讀取。然而,讀取仍然是不可重復(fù)的,并 且更新還是可能丟失。這是當(dāng)你在考慮對(duì)話的時(shí)候要面對(duì)的問(wèn)題,從用戶的觀點(diǎn)來(lái)看,這些是原子的事務(wù)。請(qǐng)見(jiàn)圖10-6。
?
|
|
|
|
|
|
|
|
|
圖10-6 對(duì)話B覆蓋對(duì)對(duì)話A所做的改變
假設(shè)兩個(gè)用戶同時(shí)選擇同一塊代碼。對(duì)話A中的用戶先 提交了變化,并且對(duì)話終止于第二個(gè)事務(wù)的成功提交。過(guò)了一會(huì)兒(可能只是一秒鐘),對(duì)話B中的用戶提交了變化。第二個(gè)事務(wù)也成功提交。在對(duì)話A中所做的改 變已經(jīng)丟失,并且(可能更糟的是)對(duì)話B中提交的數(shù)據(jù)修改可能已經(jīng)基于失效的信息。
對(duì)于如何處理對(duì)話中這些第二個(gè)事務(wù)中的丟失更新,你有3種選擇:
l??? 最晚提交生效(last commit wins)——兩個(gè)事務(wù)提交都成功,且第二次提交覆蓋第一個(gè)的變化。沒(méi)有顯示錯(cuò)誤消息。
l??? 最先提交生效(first commit wins)——對(duì)話A的事務(wù)被提交,并且在對(duì)話B中提交事務(wù)的用戶得到一條錯(cuò)誤消息。用戶必須獲取新數(shù)據(jù)來(lái)重啟對(duì)話,并再次利用沒(méi)有失效的數(shù)據(jù)完成對(duì)話的所有步驟。
l??? 合并沖突更新(merge conflicting updates)——第一個(gè)修改被提交,并且對(duì)話B中的事務(wù)在提交時(shí)終止,帶有一條錯(cuò)誤消息。但是失敗的對(duì)話B用戶可以選擇性地應(yīng)用變化,而不是再次在對(duì)話中完成所有工作。
如果你沒(méi)有啟用樂(lè)觀并發(fā)控制(默認(rèn)情況為未啟用),應(yīng)用程序就會(huì)用最晚提交生效策略運(yùn)行。在實(shí)踐中,丟失更新的這個(gè)問(wèn)題使得許多應(yīng)用程序的用戶很沮喪,因?yàn)樗麄兛赡馨l(fā)現(xiàn)他們的所有工作都丟失了,而沒(méi)有收到任何錯(cuò)誤消息。
很顯然,最先提交生效更有吸引力。如果對(duì)話B的應(yīng)用 程序的用戶提交,他就獲得這樣一條錯(cuò)誤消息:有人已經(jīng)對(duì)你要提交的數(shù)據(jù)提交了修改。你已經(jīng)使用了失效數(shù)據(jù)。請(qǐng)用新數(shù)據(jù)重啟對(duì)話。(Somebody already committed modifications to the data you’re about to commit. You’ve been working with stale data. Please restart the conversation with fresh data。)設(shè)計(jì)和編寫生成這條錯(cuò)誤消息的應(yīng)用程序,并引導(dǎo)用戶重新開(kāi)始對(duì)話,這就是你的責(zé)任了。Hibernate和Java Persistence用自動(dòng)樂(lè)觀鎖協(xié)助你,以便每當(dāng)事務(wù)試圖提交在數(shù)據(jù)庫(kù)中帶有沖突的被更新?tīng)顟B(tài)的對(duì)象時(shí),就會(huì)得到一個(gè)異常。
合并沖突的變化,是最先提交生效的一種變形。不 顯示始終強(qiáng)制用戶返回的錯(cuò)誤消息,而是提供一個(gè)對(duì)話框,允許用戶手工合并沖突的變化。這是最好的策略,因?yàn)闆](méi)有工作丟失,應(yīng)用程序的用戶也不會(huì)因?yàn)闃?lè)觀并 發(fā)失敗而受挫。然而,對(duì)于開(kāi)發(fā)人員來(lái)說(shuō),提供一個(gè)對(duì)話框來(lái)合并變化比顯示一條錯(cuò)誤消息并強(qiáng)制用戶重復(fù)所有的工作來(lái)得更加費(fèi)時(shí)。是否使用這一策略,由你自己 決定。
樂(lè)觀并發(fā)控制可以用多種方法實(shí)現(xiàn)。Hibernate使用自動(dòng)的版本控制。
2.在Hibernate中啟用版本控制
Hibernate提供自動(dòng)的版本控制。每個(gè)實(shí)體實(shí)例都有一個(gè)版本,它可以是一個(gè)數(shù)字或者一個(gè)時(shí)間戳。當(dāng)對(duì)象被修改時(shí),Hibernate就增加它的版本號(hào),自動(dòng)比較版本,如果偵測(cè)到?jīng)_突就拋出異常。因此,你給所有持久化的實(shí)體類都添加這個(gè)版本屬性,來(lái)啟用樂(lè)觀鎖:
也可以添加獲取方法;但是不許應(yīng)用程序修改版本號(hào)。XML格式的<version>屬性映射必須立即放在標(biāo)識(shí)符屬性映射之后:
版本號(hào)只是一個(gè)計(jì)數(shù)值——它沒(méi)有任何有用的語(yǔ)義值。實(shí)體表上額外的列為Hibernate應(yīng)用程序所用。記住,所有訪問(wèn)相同數(shù)據(jù)庫(kù)的其他應(yīng)用程序也可以(并且或許應(yīng)該)實(shí)現(xiàn)樂(lè)觀版本控制,并利用相同的版本列。有時(shí)候時(shí)間戳是首選(或者已經(jīng)存在):
理論上來(lái)說(shuō),時(shí)間戳更不安全一點(diǎn),因?yàn)閮蓚€(gè)并發(fā)的事務(wù)可能都在同一毫秒點(diǎn)上加載和更新同一件貨品;但在實(shí)踐中不會(huì)發(fā)生這種情況,因?yàn)镴VM通常沒(méi)有精確到毫秒(你應(yīng)該查閱JVM和操作系統(tǒng)文檔所確保的精確度)。
此外,從JVM處獲取的當(dāng)前時(shí)間在集群環(huán)境 (clustered environment)下并不一定安全,該環(huán)境中的節(jié)點(diǎn)可能不與時(shí)間同步??梢赞D(zhuǎn)換為在<timestamp>映射中利用 source="db"屬性從數(shù)據(jù)庫(kù)機(jī)器中獲取當(dāng)前的時(shí)間。并非所有的Hibernate SQL方言都支持這個(gè)屬性(檢查所配置的方言的源代碼),每一次增加版本都始終會(huì)有命中數(shù)據(jù)庫(kù)的過(guò)載。
我們建議新項(xiàng)目依賴包含版本號(hào)的版本,而不是時(shí)間戳。
一旦你把<version>或者<timestamp>屬性添加到持久化類映射,就啟用了包含版本的樂(lè)觀鎖。沒(méi)有其他的轉(zhuǎn)換。
Hibernate如何利用版本發(fā)現(xiàn)沖突?
3.版本控制的自動(dòng)管理
涉及目前被版本控制的Item對(duì)象的每一個(gè)DML操 作都包括版本檢查。例如,假設(shè)在一個(gè)工作單元中,你從版本為1的數(shù)據(jù)庫(kù)中加載一個(gè)Item。然后修改它的其中一個(gè)值類型屬性,例如Item的價(jià)格。當(dāng)持久 化上下文被清除時(shí),Hibernate偵測(cè)到修改,并把Item的版本增加到2。然后執(zhí)行SQL UPDATE使這一修改在數(shù)據(jù)庫(kù)中永久化:
如果另一個(gè)并發(fā)的工作單元更新和提交了同一個(gè) 行,OBJ_VERSION列就不再包含值1,行也不會(huì)被更新。Hibernate檢查由JDBC驅(qū)動(dòng)器返回這個(gè)語(yǔ)句所更新的行數(shù)——在這個(gè)例子中,被更 新的行數(shù)為0——并拋出StaleObjectStateException。加載Item時(shí)呈現(xiàn)的狀態(tài),清除時(shí)不再在數(shù)據(jù)庫(kù)中呈現(xiàn);因而,你正在使用失 效的數(shù)據(jù),必須通知應(yīng)用程序的用戶??梢圆蹲竭@個(gè)異常,并顯示一條錯(cuò)誤消息,或者顯示幫助用戶給應(yīng)用程序重啟對(duì)話的一個(gè)對(duì)話框。
什么樣的修改觸發(fā)實(shí)體版本的增加?每當(dāng)實(shí)體實(shí)例臟 時(shí),Hibernate就增加版本號(hào)(或者時(shí)間戳)。這包括實(shí)體的所有臟的值類型屬性,無(wú)論它們是單值、組件還是集合??紤]User和 BillingDetails之間的關(guān)系,這是個(gè)一對(duì)多的實(shí)體關(guān)聯(lián):如果CreditCard修改了,相關(guān)的User版本并沒(méi)有增加。如果你從賬單細(xì)節(jié)的 集合中添加或者刪除CreditCard(或者BankAccount),User的版本就增加了。
如果你想要禁用對(duì)特定值類型屬性或者集合的自動(dòng)增加,就用optimistic-lock="false"屬性映射它。inverse屬性在這里沒(méi)有什么區(qū)別。甚至如果元素從反向集合中被添加或者移除,反向集合的所有者的版本也會(huì)被更新。
如你所見(jiàn),Hibernate使得對(duì)于樂(lè)觀并發(fā)控制管理版本變得難以置信地輕松。如果你正在使用遺留數(shù)據(jù)庫(kù)Schema或者現(xiàn)有的Java類,也許不可能引入版本或者時(shí)間戳和列。Hibernate提供了另一種可選的策略。
4.沒(méi)有版本號(hào)或者時(shí)間戳的版本控制
如果你沒(méi)有版本或者時(shí)間戳列,Hibernate仍然能夠執(zhí)行自動(dòng)的版本控制,但是只對(duì)在同一個(gè)持久化上下文中獲取和修改的對(duì)象(即相同的Session)。如果你需要樂(lè)觀鎖用于通過(guò)脫管對(duì)象實(shí)現(xiàn)的對(duì)話,則必須使用通過(guò)脫管對(duì)象傳輸?shù)陌姹咎?hào)或者時(shí)間戳。
這種可以選擇的版本控制實(shí)現(xiàn)方法,在獲取對(duì)象(或者最后一次清除持久化上下文)時(shí),把當(dāng)前的數(shù)據(jù)庫(kù)狀態(tài)與沒(méi)有被修改的持久化屬性值進(jìn)行核對(duì)。可以在類映射中通過(guò)設(shè)置optimistic-lock屬性來(lái)啟用這項(xiàng)功能:
下列SQL現(xiàn)在被執(zhí)行,用來(lái)清除Item實(shí)例的修改:
Hibernate在SQL語(yǔ)句的WHERE子句 中,列出了所有列和它們最后知道的非失效值。如果任何并發(fā)的事務(wù)已經(jīng)修改了這些值中的任何一個(gè),或者甚至刪除了行,這個(gè)語(yǔ)句就會(huì)再次返回被更新的行數(shù)為 0。然后Hibernate拋出一個(gè)StaleObjectStateException。
另一種方法是,如果設(shè)置optimistic- lock="dirty",Hibernate只包括限制中被修改的屬性(在這個(gè)例子中,只有ITEM_PRICE)。這意味著兩個(gè)工作單元可以同時(shí)修改 同一個(gè)對(duì)象,并且只有當(dāng)兩者修改同一個(gè)值類型屬性(或者外鍵值)時(shí)才會(huì)偵測(cè)到?jīng)_突。在大多數(shù)情況下,這對(duì)于業(yè)務(wù)實(shí)體來(lái)說(shuō)并不是一種好策略。想象有兩個(gè)人同 時(shí)修改一件拍賣貨品:一個(gè)改變價(jià)格,另一個(gè)改變描述。即使這些修改在最低級(jí)別(數(shù)據(jù)庫(kù)行)沒(méi)有沖突,從業(yè)務(wù)邏輯觀點(diǎn)看它們也可能發(fā)生沖突。如果貨品的描述 完全改變了,還可以改變它的價(jià)格嗎?如果你想要使用這個(gè)策略,還必須在實(shí)體的類映射上啟用dynamic- update="true",Hibernate無(wú)法在啟動(dòng)時(shí)給這些動(dòng)態(tài)的UPDATE語(yǔ)句生成SQL。
不建議在新應(yīng)用程序中定義沒(méi)有版本或者時(shí)間戳列的版本控制;它更慢、更復(fù)雜,如果你正在使用脫管對(duì)象,則它不會(huì)生效。
Java Persistence應(yīng)用程序中的樂(lè)觀并發(fā)控制與Hibernate中的幾乎如出一轍。
5.用Java Persistence版本控制
Java Persistence規(guī)范假設(shè)并發(fā)數(shù)據(jù)訪問(wèn)通過(guò)版本控制被樂(lè)觀處理。為了給一個(gè)特定的實(shí)體啟用自動(dòng)版本控制,需要添加一個(gè)版本屬性或者字段:
同樣地,可以公開(kāi)一個(gè)獲取方法,但不能允許應(yīng)用程序 修改版本值。在Hibernate中,實(shí)體的版本屬性可以是任何數(shù)字類型,包括基本類型,或者Date或者Calendar類型。JPA規(guī)范只把int、 Integer、short、Short、long、Long和java.sql.Timestamp當(dāng)作可移植的版本類型。
由于JPA標(biāo)準(zhǔn)沒(méi)有涵蓋無(wú)版本屬性的樂(lè)觀版本控制,因此需要Hibernate擴(kuò)展,通過(guò)對(duì)比新舊狀態(tài)來(lái)啟用版本控制:
如果只是希望在版本檢查期間比較被修改的屬性,也可以轉(zhuǎn)換到OptimisticLockType. DIRTY。然后你還需要設(shè)置dynamicUpdate屬性為true。
Java Persistence沒(méi)有對(duì)哪個(gè)實(shí)體實(shí)例修改應(yīng)該觸發(fā)版本增加標(biāo)準(zhǔn)化。如果你用Hibernate作為JPA提供程序,默認(rèn)是一樣的——每一個(gè)值類型的 屬性修改(包括集合元素的添加和移除)都觸發(fā)版本增加。在編寫本書(shū)之時(shí),還沒(méi)有在特定的屬性和集合上禁用版本增加的Hibernate注解,但是已經(jīng)存在 一項(xiàng)對(duì)@OptimisticLock(excluded=true)的特性請(qǐng)求。你的Hibernate Annotations版本或許包括了這個(gè)選項(xiàng)。
Hibernate EntityManager,像任何其他Java Persistence提供程序一樣,當(dāng)偵測(cè)到?jīng)_突版本時(shí),就拋出 javax.persistence.OptimisticLockException。這相當(dāng)于Hibernate中原生的Stale- ObjectStateException,因此應(yīng)該進(jìn)行相應(yīng)處理。
我們現(xiàn)在已經(jīng)涵蓋了數(shù)據(jù)庫(kù)連接的基礎(chǔ)隔離性級(jí)別,結(jié) 論是你通常應(yīng)該依賴來(lái)自數(shù)據(jù)庫(kù)的讀取提交保證。Hibernate和Java Persistence中的自動(dòng)版本控制,在兩個(gè)并發(fā)事務(wù)試圖在同一塊代碼中提交修改時(shí),防止了丟失更新。為了處理非可重復(fù)讀取,你需要額外的隔離性保 證。
10.2.3? 獲得額外的隔離性保證
有幾種方法防止不可重復(fù)讀取,并升級(jí)到一個(gè)更高的隔離性級(jí)別。
1.顯式的悲觀鎖
已經(jīng)討論了把所有的數(shù)據(jù)庫(kù)連接轉(zhuǎn)換到一個(gè)比讀取提交 更高的隔離性級(jí)別,但我們的結(jié)論是,當(dāng)關(guān)注應(yīng)用程序的可伸縮性時(shí),這則是一項(xiàng)糟糕的默認(rèn)。你需要更好、僅用于一個(gè)特定的工作單元的隔離性保證。還要記住, 持久化上下文高速緩存為處于持久化狀態(tài)的實(shí)體實(shí)例提供可重復(fù)讀取。然而,這并非永遠(yuǎn)都是足夠的。
例如,對(duì)標(biāo)量查詢(scalar query)可能需要可重復(fù)讀?。?
這個(gè)工作單元執(zhí)行兩次讀取。第一次通過(guò)標(biāo)識(shí)符獲取實(shí) 體實(shí)例。第二次讀取標(biāo)量查詢,再次加載已經(jīng)加載的Item實(shí)體的描述。在這個(gè)工作單元中有一個(gè)小窗口,在那里,并發(fā)運(yùn)行的事務(wù)可以在兩次讀取之間提供一個(gè) 更新過(guò)的貨品描述。然后第二次讀取返回這個(gè)提交數(shù)據(jù),且變量description有一個(gè)與屬性i.getDescription()不同的值。
這個(gè)例子進(jìn)行過(guò)簡(jiǎn)化,但仍然足以說(shuō)明:如果數(shù)據(jù)庫(kù)事務(wù)隔離性級(jí)別是讀取提交,那么混有實(shí)體和標(biāo)量讀取的工作單元有多么容易受到非可重復(fù)讀取的影響。
不是把所有的數(shù)據(jù)庫(kù)事務(wù)轉(zhuǎn)換為一個(gè)更高的、不可伸縮的隔離性級(jí)別,而是在必要時(shí),在Hibernate Session中使用lock()方法獲得更強(qiáng)的隔離性保證:
使用LockMode.UPGRADE,給表示Item實(shí)例的(多)行,促成了在數(shù)據(jù)庫(kù)中保存的悲觀鎖。現(xiàn)在沒(méi)有并發(fā)事務(wù)可以在相同數(shù)據(jù)中獲得鎖——也就是說(shuō),沒(méi)有并發(fā)事務(wù)可以在你的兩次讀取之間修改數(shù)據(jù)。這段代碼可以被縮短成如下:
LockMode.UPGRADE導(dǎo)致一個(gè) SQL SELECT ... FOR UPDATE或者類似的東西,具體取決于數(shù)據(jù)庫(kù)方言。一種變形LockMode.UPGRADE_NOWAIT,添加了一個(gè)允許查詢立即失敗的子句。如果 沒(méi)有這個(gè)子句,當(dāng)無(wú)法獲得鎖時(shí)(可能由于一個(gè)并發(fā)事務(wù)已經(jīng)有鎖),數(shù)據(jù)庫(kù)通常會(huì)等待。等待的持續(xù)時(shí)間取決于數(shù)據(jù)庫(kù),就像實(shí)際的SQL子句一樣。
常見(jiàn)問(wèn)題 可以使用長(zhǎng)悲觀鎖嗎?在Hibernate中,悲 觀鎖的持續(xù)時(shí)間是單個(gè)數(shù)據(jù)庫(kù)事務(wù)。這意味著你無(wú)法使用專門的鎖,來(lái)阻塞比單個(gè)數(shù)據(jù)庫(kù)事務(wù)更長(zhǎng)的并發(fā)訪問(wèn)。我們認(rèn)為這是好的一面,因?yàn)閷?duì)于比如整個(gè)會(huì)話的持 續(xù)時(shí)間來(lái)說(shuō),唯一的解決方案將是在內(nèi)存(或者數(shù)據(jù)庫(kù)中所謂的鎖定表,lock table)中保存一個(gè)非常昂貴的鎖。這種鎖有時(shí)也稱作離線(offline)鎖。這通常是個(gè)性能瓶頸;每個(gè)數(shù)據(jù)訪問(wèn)都要對(duì)一個(gè)同步鎖管理器進(jìn)行額外的鎖 檢查。然而,樂(lè)觀鎖是最完美的并發(fā)控制策略,并且在長(zhǎng)運(yùn)行對(duì)話中執(zhí)行得很好。根據(jù)你的沖突解析(conflict-resolution)選項(xiàng)(即如果你 有足夠的時(shí)間實(shí)現(xiàn)合并變化),你應(yīng)用程序的用戶對(duì)此將會(huì)像對(duì)被阻塞的并發(fā)訪問(wèn)一樣滿意。他們也可能感激當(dāng)其他人在看相同數(shù)據(jù)時(shí),自己沒(méi)有被鎖在特定的屏幕 之外。
Java Persistence出于同樣的目的定義了LockModeType.READ,且EntityManager也有一個(gè)lock()方法。規(guī)范沒(méi)有要求 未被版本控制的實(shí)體支持這種鎖模式;但Hibernate在所有的實(shí)體中都支持它,因?yàn)樗跀?shù)據(jù)庫(kù)中默認(rèn)為悲觀鎖。
2.Hibernate鎖模式
Hibernate支持下列其他LockMode:
l??? LockMode.NONE——?jiǎng)e到數(shù)據(jù)庫(kù)中去,除非對(duì)象不處于任何高速緩存中。
l??? LockMode.READ——繞過(guò)所有高速緩存,并執(zhí)行版本檢查,來(lái)驗(yàn)證內(nèi)存中的對(duì)象是否與當(dāng)前數(shù)據(jù)庫(kù)中存在的版本相同。
l??? LockMode.UPGRADE——繞過(guò)所有高速緩存,做一個(gè)版本檢查(如果適用),如果支持的話,就獲得數(shù)據(jù)庫(kù)級(jí)的悲觀升級(jí)鎖。相當(dāng)于Java Persistence中的LockModeType.READ。如果數(shù)據(jù)庫(kù)方言不支持SELECT ... FOR UPDATE選項(xiàng),這個(gè)模式就透明地退回到LockMode.READ。
l??? LockMode.UPGRADE_NOWAIT——與UPGRADE相同,但如果支持的話,就使用SELECT ... FOR UPDATE NOWAIT。它禁用了等待并發(fā)鎖釋放,因而如果無(wú)法獲得鎖,就立即拋出鎖異常。如果數(shù)據(jù)庫(kù)SQL方言不支持NOWAIT選項(xiàng),這個(gè)模式就透明地退回到 LockMode.UPGRADE。
l??? LockMode.FORCE——在數(shù)據(jù)庫(kù)中強(qiáng)制增加對(duì)象的版本,來(lái)表明它已經(jīng)被當(dāng)前事務(wù)修改。相當(dāng)于Java Persistence中的LockModeType.WRITE。
l??? LockMode.WRITE——當(dāng)Hibernate已經(jīng)在當(dāng)前事務(wù)中寫到一個(gè)行時(shí),就自動(dòng)獲得它。(這是一種內(nèi)部模式;你不能在應(yīng)用程序中指定它。)
默認(rèn)情況下,load()和get()使用LockMode.NONE。LockMode.READ對(duì)session.lock()和脫管對(duì)象最有用。這里有個(gè)例子:
這段代碼在通過(guò)級(jí)聯(lián)(假設(shè)從Item到Bid的關(guān)聯(lián)啟用了級(jí)聯(lián))保存新Bid之前,在脫管的Item實(shí)例上執(zhí)行版本檢查,驗(yàn)證該數(shù)據(jù)庫(kù)行在獲取之后沒(méi)有被另一個(gè)事務(wù)更新。
(注意,EntityManager.lock()不重附指定的實(shí)體實(shí)例——它只對(duì)已經(jīng)處于托管持久化狀態(tài)的實(shí)例有效。)
Hibernate LockMode.FORCE和Java Persistence中的LockModeType.WRITE有著不同的用途。如果默認(rèn)不增加版本,就利用它們強(qiáng)制版本更新。
3.強(qiáng)制增加版本
如果通過(guò)版本控制啟用樂(lè)觀鎖,Hibernate會(huì)自動(dòng)增加被修改實(shí)體實(shí)例的版本。然而,有時(shí)你想手工增加實(shí)體實(shí)例的版本,因?yàn)镠ibernate不會(huì)把你的改變當(dāng)成一個(gè)應(yīng)該觸發(fā)版本增加的修改。
想象你修改了CreditCard所有者的名稱:
當(dāng)這個(gè)Session被清除時(shí),被修改的BillingDetails實(shí)例(我們假設(shè)是一張信用卡)的版本通過(guò)Hibernate自動(dòng)增加了。這可能并不是你想要的東西——你可能也想增加所有者(User實(shí)例)的版本。
用LockMode.FORCE調(diào)用lock(),增加一個(gè)實(shí)體實(shí)例的版本:
現(xiàn)在,任何使用相同User行的并發(fā)工作單元都 知道這個(gè)數(shù)據(jù)被修改了,即使只有被你認(rèn)為是整個(gè)聚合的一部分的其中一個(gè)值被修改了。這種技術(shù)在許多情況下都有用,例如當(dāng)你修改一個(gè)對(duì)象,并且想要增加聚合 的根對(duì)象版本時(shí)。另一個(gè)例子是對(duì)一件拍賣貨品出價(jià)金額的修改(如果這些金額是不可變的):利用一個(gè)顯式的版本增加,可以指出這件貨品已經(jīng)被修改,即使它的 值類型屬性或者集合都沒(méi)有發(fā)生改變。利用Java Persistence的同等調(diào)用是em.lock(o,LockModeType.WRITE)。
現(xiàn)在,你具備了編寫更復(fù)雜工作單元和創(chuàng)建對(duì)話的所有知識(shí)。但是,我們需要提及事務(wù)的最后一個(gè)方面,因?yàn)樗谑褂肑PA的更復(fù)雜對(duì)話中變得必不可少。你必須理解自動(dòng)提交如何工作,以及在實(shí)踐中非事務(wù)數(shù)據(jù)訪問(wèn)意味著什么。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺(jué)我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
