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

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