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

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