跳轉至內容

Java 持久化/鎖定

來自 Wikibooks,開放世界中的開放書籍

鎖定可能是最常被忽略的持久化考慮因素。大多數應用程式在開發過程中往往忽略了併發問題,然後在投入生產之前才勉強新增鎖定機制。考慮到軟體專案中很大一部分的失敗或取消,或者從未獲得大量使用者群,也許這合乎邏輯。但是,鎖定和併發對於大多數應用程式來說是一個關鍵問題,或者至少是一個非常重要的因素,因此可能應該在開發週期的早期階段進行考慮。

如果應用程式將對同一個物件進行併發寫入,那麼鎖定策略至關重要,以便防止資料損壞。有兩種策略可以防止對同一物件/行進行併發修改:樂觀悲觀 鎖定。從技術上講,還存在第三種策略,鴕鳥 鎖定,或者不鎖定,這意味著把頭埋在沙子裡,忽略這個問題。

實現樂觀和悲觀鎖定的方式有很多。JPA 支援版本樂觀鎖定,但一些 JPA 提供者也支援其他樂觀鎖定方法,以及悲觀鎖定。

鎖定和併發可能是一個令人困惑的因素,而且存在很多誤解。在應用程式中正確實現鎖定通常需要做的不僅僅是設定一些 JPA 或資料庫配置選項(儘管對於那些認為正在使用鎖定的應用程式來說,這確實是他們所做的全部)。鎖定可能還需要應用程式級別的更改,並確保訪問資料庫的其他應用程式也以正確的鎖定策略進行操作。

樂觀鎖定

[編輯 | 編輯原始碼]

樂觀鎖定假設在您讀取資料到寫入資料之間,資料不會被修改。這是當今持久化解決方案中最常用且推薦的鎖定方式。該策略涉及檢查原始讀取物件中的一個或多個值在更新時是否仍然相同。這驗證了在讀取和寫入之間,該物件沒有被其他使用者更改。

JPA 支援使用樂觀鎖定的版本欄位,該欄位在每次更新時都會更新。該欄位可以是數字或時間戳值。建議使用數字,因為數字值更精確、可移植、效能更高,並且比時間戳更容易處理。

@Version 註釋或 <version> 元素用於定義樂觀鎖定版本欄位。該註釋定義在物件的版本欄位或屬性上,類似於 Id 對映。該物件必須包含一個屬性來儲存版本欄位。

物件的版本屬性會由 JPA 提供者自動更新,通常不應由應用程式修改。唯一的例外是,如果應用程式在一個事務中讀取物件,將物件傳送到客戶端,並在另一個事務中更新/合併物件。在這種情況下,應用程式必須確保使用原始物件版本,否則在讀取和寫入之間的任何更改都將無法檢測到。EntityManager merge() API 將始終合併版本,因此只有在手動合併時,應用程式才負責此操作。

當檢測到鎖定爭用時,將丟擲 OptimisticLockException。這可能包裝在 RollbackException 或其他異常中(如果使用 JTA),但它應該設定為異常的 cause。應用程式可以處理異常,但通常應向用戶報告錯誤,並讓他們決定如何操作。

版本註釋示例

[編輯 | 編輯原始碼]
@Entity
public abstract class Employee{
  @Id
  private long id;
  @Version
  private long version;
  ...
}

版本 XML 示例

[編輯 | 編輯原始碼]
<entity name="Employee" class="org.acme.Employee" access="FIELD">
    <attributes>
        <id name="id"/>
        <version name="version"/>
        ...
    </attributes>
<entity/>

常見的鎖定錯誤、問題和解答

[編輯 | 編輯原始碼]

不將版本傳送到客戶端,僅在伺服器上鎖定

[編輯 | 編輯原始碼]
在鎖定方面,最常見的錯誤可能是鎖定了錯誤的程式碼部分。無論使用何種形式的鎖定,無論是樂觀還是悲觀,情況都是如此。基本情況是
  1. 使用者請求一些資料,伺服器從資料庫中讀取資料並將其傳送到使用者的客戶端(無論客戶端是 html、rmi 還是 web 服務)。
  2. 使用者在客戶端編輯資料。
  3. 使用者將資料提交回伺服器。
  4. 伺服器開始一個事務,讀取物件,合併資料並提交事務。
問題在於原始資料是在步驟 1 中讀取的,但直到步驟 4 才獲得鎖定,因此在步驟 1 和 4 之間對物件所做的任何更改都不會導致衝突。這意味著使用任何鎖定都沒有什麼意義。
關鍵點在於,當使用資料庫悲觀鎖定或資料庫事務隔離時,情況總是如此,資料庫鎖定只會在步驟 4 中發生,任何衝突都無法檢測到。這是使用資料庫鎖定無法擴充套件到 web 應用程式的主要原因,因為要使鎖定有效,必須在步驟 1 開始資料庫事務,並在步驟 4 之前不提交。這意味著在等待 web 客戶端時,必須保持一個活動資料庫連線和事務處於開啟狀態,以及鎖定,因為無法保證 web 客戶端不會在資料上停留數小時、去吃午飯或消失,從而佔用資料庫資源並鎖定所有其他使用者的資料,這可能非常不可取。
對於樂觀鎖,解決方案相對簡單,物件的版本必須與資料一起傳送到客戶端(或儲存在 http 會話中)。當用戶提交資料回來時,原始版本必須與從資料庫中讀取的物件合併,以確保步驟 1 和 4 之間發生的任何更改都會被檢測到。

處理樂觀鎖異常

[編輯 | 編輯原始碼]
不幸的是,程式設計師經常聰明反被聰明誤。使用樂觀鎖時,第一個問題是當出現 OptimisticLockException 時該怎麼辦。友好鄰里超級程式設計師的典型反應是自動處理異常。他們只需建立一個新的事務,重新整理物件以重置其版本,並將資料合併回物件並重新提交。瞧,問題解決了,是嗎?
這實際上違背了鎖定的初衷。如果你想要這樣,你也可以不用任何鎖定。不幸的是,OptimisticLockException 應該很少被自動處理,你需要真正地打擾使用者來解決這個問題。你應該向使用者報告衝突,並要麼說“對不起,發生了編輯衝突,他們需要重做他們的工作”,要麼在最好的情況下,重新整理物件並向用戶展示當前資料和他們提交的資料,並幫助他們在適當的情況下合併這兩個資料。
一些自動合併工具會比較兩個衝突的資料版本,如果沒有任何單個欄位衝突,那麼資料就會在沒有使用者幫助的情況下自動合併。這是大多數軟體版本控制系統所做的。不幸的是,使用者通常比程式更能決定何時發生衝突,僅僅因為兩個版本的 .java 檔案沒有更改相同的程式碼行並不意味著沒有衝突,第一個使用者可能刪除了另一個使用者新增的方法引用的方法,以及其他一些可能導致通常的夜間構建偶爾中斷的問題。

偏執妄想

[編輯 | 編輯原始碼]
鎖定可以防止大多數併發問題,但要注意不要過度分析所有可能的假設情況。有時,在一個併發應用程式(或任何軟體應用程式)中,可能會發生不好的事情。使用者現在已經習慣了這一點,我認為沒有人認為計算機是完美的。
一個很好的例子是原始碼控制系統。允許使用者互相覆蓋更改是一件壞事;因此,大多數系統透過對原始檔進行版本控制來避免這種情況。如果使用者提交對源自舊版本的檔案的更改,原始碼控制系統會引發衝突並讓使用者合併這兩個檔案。這本質上是樂觀鎖。但是,如果一個使用者在一個檔案中刪除或重新命名了一個方法,然後另一個使用者在另一個檔案中添加了一個新的方法或呼叫到該舊方法?我所知道的沒有任何原始碼控制系統會檢測到這個問題,這是一個衝突,會導致構建失敗。解決這個問題的辦法是開始鎖定或檢查系統中每個檔案(或者至少每個可能相關的檔案)的鎖。類似於對每個可能相關的物件使用樂觀讀鎖,或者悲觀地鎖定每個可能相關的物件。這可以做到,但這可能會非常昂貴,更重要的是,它現在會在使用者每次簽入時都會引發可能的衝突,因此它完全沒有用。
因此,總的來說,要注意不要過度偏執,以至於你犧牲了系統的可用性。

其他應用程式訪問相同資料

[編輯 | 編輯原始碼]
任何有效的鎖定形式都需要所有訪問相同資料的應用程式遵循相同的規則。如果你在一個應用程式中使用樂觀鎖,但在另一個訪問相同資料的應用程式中不使用任何鎖定,它們仍然會發生衝突。一個假的解決方案是配置一個更新觸發器,始終遞增版本值(除非在更新中遞增)。這將允許新的應用程式避免覆蓋舊應用程式的更改,但舊應用程式仍然能夠覆蓋新應用程式的更改。這仍然可能比不使用任何鎖定好,也許舊應用程式最終會消失。
一個常見的誤解是,如果你使用悲觀鎖,而不是新增一個版本欄位,你就會沒事。同樣,悲觀鎖要求所有訪問相同資料的應用程式使用相同的鎖定形式。舊應用程式仍然可以讀取資料(不鎖定),然後在新應用程式讀取、鎖定和更新相同資料後更新資料,從而覆蓋其更改。

資料庫事務隔離不是我需要的嗎?

[編輯 | 編輯原始碼]
也許,但很可能不是。大多數資料庫預設使用已提交讀事務隔離。這意味著你永遠不會看到未提交的資料,但這並不能防止併發事務覆蓋相同資料。
  1. 事務 A 讀取行 x。
  2. 事務 B 讀取行 x。
  3. 事務 A 寫入行 x。
  4. 事務 B 寫入行 x(並覆蓋 A 的更改)。
  5. 兩者都成功提交。
這是已提交讀的情況,但使用可序列化,這種衝突就不會發生。使用可序列化,要麼事務 B 會在 B 的選擇上鎖定並等待(可能很長時間),直到事務 A 提交。在一些資料庫中,事務 A 可能不會等待,但在提交時會失敗。但是,即使使用可序列化隔離,典型的 Web 應用程式仍然會發生衝突。這是因為每個伺服器請求都在不同的資料庫事務中執行。Web 客戶端在一個事務中讀取資料,然後在另一個事務中更新資料。因此,樂觀鎖實際上是典型 Web 應用程式中唯一可行的鎖定選項。即使讀寫發生在同一個事務中,可序列化通常也不是解決方案,因為併發性影響和死鎖的可能性。
參見 可序列化事務隔離

如果我合併了一個被另一個使用者刪除的物件會發生什麼?

[編輯 | 編輯原始碼]
應該發生的是,合併應該觸發 OptimisticLockException,因為物件有一個非空的版本,並且大於 0,並且物件不存在。但這可能是 JPA 提供商特有的,有些可能會重新插入物件(這會在沒有鎖定的情況下發生),或者丟擲不同的異常。
如果你呼叫了 persist 而不是合併,那麼物件就會被重新插入。

如果我的表沒有版本列?

[編輯 | 編輯原始碼]
最好的解決方案可能是新增一個。欄位鎖定是另一個解決方案,在某些情況下,悲觀鎖定也是解決方案。
參見 欄位鎖定

關係怎麼辦?

[編輯 | 編輯原始碼]
參見 級聯鎖定

我可以使用時間戳嗎?

[編輯 | 編輯原始碼]
參見 時間戳鎖定

對於繼承或多個表,我是否需要在每個表中都有一個版本?

[edit | edit source]
簡短的回答是不,只需要在根表中。
參見多個版本

高階

[edit | edit source]

時間戳鎖

[edit | edit source]

時間戳版本鎖由 JPA 支援,配置方式與數值版本鎖相同,區別在於屬性型別將為 java.sql.Timestamp 或其他日期/時間型別。在使用時間戳鎖時要謹慎,因為不同資料庫中的時間戳精度不同,有些資料庫不儲存時間戳的毫秒,或者不精確地儲存。一般來說,時間戳鎖不如數值版本鎖高效,因此建議使用數值版本鎖。

如果表中已經存在一個上次更新時間戳列,則經常使用時間戳鎖,這也是自動更新上次更新列的一種便捷方式。時間戳版本值比數值版本更有用,因為它包含有關物件上次更新時間的相關資訊。

時間戳版本鎖中的時間戳值可以來自資料庫,也可以來自 Java(中間層)。JPA 不允許配置此選項,但一些 JPA 提供程式可能提供此選項。使用資料庫的當前時間戳可能非常昂貴,因為它需要對伺服器進行資料庫呼叫。

多個版本

[edit | edit source]

一個物件在 JPA 中只能有一個版本。即使物件對映到多個表,也只有主表具有版本。如果任何表的任何欄位發生更改,版本將被更新。如果您需要多個版本,您可能需要在物件中對映多個版本屬性並手動維護重複版本,也許可以透過事件。從技術上講,沒有什麼可以阻止您為多個屬性新增 @Version 註解,並且一些 JPA 提供程式可能支援這一點。

級聯鎖定

[edit | edit source]

鎖定物件不同於鎖定資料庫中的行。物件可能比簡單的行更復雜;物件可以跨越多個表,具有繼承關係、關係和依賴物件。因此,確定物件何時更改並需要更新其版本可能比確定行何時更改更難。

JPA 確實定義了當物件的任何表發生更改時更新版本。但是關於關係還不清楚。如果 BasicEmbedded 或外部索引鍵關係 (OneToOneManyToOne) 發生更改,版本將被更新。但 OneToManyManyToMany 和目標外部索引鍵 OneToOne 呢?對於這些關係的更改,版本更新可能取決於 JPA 提供程式。

依賴物件的更改呢?JPA 沒有為鎖定定義級聯選項,也沒有直接的依賴物件概念,因此這不是一種選擇。一些 JPA 提供程式可能支援這一點。模擬此操作的一種方法是使用 寫鎖定。JPA 定義了 EntityManager lock() API。您可以在根父物件中定義一個版本,當子物件(或關係)發生更改時,您可以使用父物件呼叫 lock API 以觸發 WRITE 鎖定。這將導致父版本的更新。您還可以透過持久化事件自動執行此操作。

級聯鎖定的使用取決於您的應用程式。如果在您的應用程式中,您認為一個使用者更新物件的某個依賴部分,而另一個使用者更新物件的另一個部分是一種鎖定衝突,那麼這就是您想要的。如果您的應用程式不認為這是一種問題,那麼您不需要級聯鎖定。級聯鎖定的一大優勢是您需要維護的版本欄位更少,並且只需要更新根物件才能檢查版本。這在批次寫入等最佳化中可能會有所不同,因為如果依賴物件具有必須檢查的自己的版本,則可能無法進行批次寫入。

TopLink / EclipseLink : 透過他們的 @OptimisticLocking@PrivateOwned 註解和 XML 支援級聯鎖定。

欄位鎖定

[edit | edit source]

如果您在表中沒有版本欄位,樂觀欄位鎖是另一種解決方案。欄位鎖定涉及在更新時比較物件中的某些欄位。如果這些欄位已更改,則更新將失敗。JPA 不支援欄位鎖定,但一些 JPA 提供程式支援它。

當需要更精細的鎖定級別時,也可以使用欄位鎖定。例如,如果一個使用者更改物件的名稱,而另一個使用者更改物件的地址,您可能希望這些更新不發生衝突,並且只希望在使用者更改相同欄位時出現樂觀鎖錯誤。您可能還只關心對某些欄位更改的衝突,並且不希望從其他欄位的衝突中出現鎖錯誤。

欄位鎖定也可以用於舊版模式,其中您無法新增版本列,或者與其他應用程式整合,這些應用程式訪問相同的資料但未使用樂觀鎖定(注意,如果其他應用程式也未使用欄位鎖定,則您只能檢測一個方向的衝突)。

欄位鎖定有幾種型別

  • 在更新中比較所有欄位 - 這會導致 where 子句非常大,但會檢測到任何衝突。
  • 在更新中比較選定欄位 - 這在只需要某些欄位的衝突時很有用。
  • 在更新中比較已更改欄位 - 這在僅將相同欄位的更改視為衝突時很有用。

如果您的 JPA 提供程式不支援欄位鎖定,則很難模擬,因為它需要更改更新 SQL。您的 JPA 提供程式可能允許覆蓋更新 SQL,在這種情況下,可能可以使用 AllSelected 欄位鎖定(如果您有權訪問原始值),但 Changed 欄位鎖定更難,因為更新必須是動態的。模擬欄位鎖定的另一種方法是 flush 您的更改,然後使用單獨的 EntityManager 和連線重新整理物件,並將當前值與原始物件進行比較。

使用欄位鎖定時,務必保留讀取的原始物件。如果您在一個事務中讀取物件並將其傳送到客戶端,然後在另一個事務中更新,那麼您實際上並沒有鎖定。在讀取和寫入之間進行的任何更改都不會被檢測到。您必須將讀取的原始物件保留在 EntityManager 中,以使您的鎖定生效。

TopLink / EclipseLink : 透過他們的 @OptimisticLocking 註解和 XML 支援欄位鎖定。

讀寫鎖定

[edit | edit source]

有時希望鎖定您未更改的內容。通常,這是在對一個物件進行更改時完成的,該更改基於另一個物件的狀態,並且您希望確保另一個物件在提交時表示資料庫的當前狀態。這就是可序列化事務隔離為您提供的功能,但樂觀的讀寫鎖定允許以宣告性和樂觀的方式(並且沒有死鎖、併發和開啟事務問題)滿足此要求。

JPA 透過 EntityManager.lock() API 支援讀寫鎖。 LockModeType 引數可以是 READWRITEREAD 鎖定將確保物件的狀態在提交時不會發生更改。WRITE 鎖定將確保此事務與任何其他更改或鎖定物件的事務發生衝突。本質上,READ 鎖定檢查樂觀版本欄位,而 WRITE 鎖定檢查並遞增它。

使用 Lock API 的示例

[edit | edit source]
Employee employee = entityManager.find(Employee.class, id);
employee.setSalary(employee.getManager().getSalary() / 2);
entityManager.lock(employee.getManager(), LockModeType.READ);

寫鎖定也可以用於提供物件級鎖定。如果您希望對依賴物件的更改與對父物件的任何更改或任何其他依賴物件的更改發生衝突,可以透過寫鎖定來實現。這也可用於鎖定關係,當您更改 OneToMany 或 ManyToMany 關係時,您還可以強制父版本的遞增。

使用 Lock API 進行級聯鎖定的示例

[edit | edit source]
Employee employee = entityManager.find(Employee.class, id);
employee.getAddress().setCity("Ottawa");
entityManager.lock(employee, LockModeType.WRITE);

無鎖定,又稱鴕鳥鎖定

[編輯 | 編輯原始碼]

從概念上講,人們可能會嘲笑或對沒有鎖定的想法感到震驚,但這可能是最常見的鎖定形式。有些人稱之為鴕鳥鎖定,因為策略是將頭埋在沙子裡,忽略問題。大多數原型或小型應用程式通常沒有鎖定要求,在大多數情況下也不需要鎖定,處理鎖定爭用發生時的操作超出了應用程式的範圍,因此最好忽略這個問題。

一般來說,在 JPA 中始終啟用樂觀鎖定可能是最好的選擇,因為它在概念上很容易實現,但如果沒有任何形式的鎖定,衝突時會發生什麼?本質上是最後寫入者獲勝,因此,如果兩個使用者同時編輯同一個物件,最後一個提交的使用者將看到其更改反映在資料庫中。這對於使用者編輯相同欄位的情況是正確的,但如果兩個使用者編輯同一個物件中的不同欄位,則取決於 JPA 實現。一些 JPA 提供程式只更新更改的欄位,而其他提供程式更新物件中的所有欄位。因此,在一種情況下,第一個使用者的更改將被覆蓋,但在第二種情況下,他們不會被覆蓋。

悲觀鎖定

[編輯 | 編輯原始碼]

悲觀鎖定意味著在開始編輯物件之前獲取物件的鎖,以確保沒有其他使用者正在編輯該物件。悲觀鎖定通常透過使用資料庫行鎖來實現,例如透過 SELECT ... FOR UPDATE SQL 語法。資料被讀取和鎖定,更改被執行,事務被提交,並釋放鎖。

JPA 1.0 不支援悲觀鎖定,但一些 JPA 1.0 提供程式支援。 JPA 2.0 支援悲觀鎖定。也可以使用 JPA 本地 SQL 查詢來發出 SELECT ... FOR UPDATE 並使用悲觀鎖定。使用悲觀鎖定時,必須確保在鎖定物件時重新整理該物件,鎖定可能過時的物件毫無用處。悲觀鎖定的 SQL 語法是特定於資料庫的,不同的資料庫具有不同的語法和支援級別,因此請確保您的資料庫正確支援您的鎖定要求。

EclipseLink(截至 1.2): 支援 JPA 2.0 悲觀鎖定。
TopLink / EclipseLink: 透過 "eclipselink.pessimistic-lock" 查詢提示支援悲觀鎖定。

悲觀鎖定的主要問題是它們使用資料庫資源,因此需要在編輯期間保持資料庫事務和連線開啟。這通常不適合互動式 Web 應用程式。悲觀鎖定也會出現併發問題並導致死鎖。悲觀鎖定的主要優點是,一旦獲取鎖,就相當確定編輯將成功。這在高度併發應用程式中可能很理想,在這些應用程式中,樂觀鎖定可能會導致太多樂觀鎖定錯誤。

還有其他方法可以實現悲觀鎖定,它可以在應用程式級別或透過 可序列化事務 隔離來實現。

應用程式級別的悲觀鎖定可以透過在物件中新增一個 locked 欄位來實現。在編輯之前,您必須將該欄位更新為 locked(並提交更改)。然後,您可以編輯物件並將 locked 欄位設定回 false。為了避免在獲取鎖時發生衝突,您還應該使用樂觀鎖定,以確保鎖欄位不會被另一個使用者同時更新為 true。

JPA 2.0 鎖定

[編輯 | 編輯原始碼]

JPA 2.0 添加了對悲觀鎖定的支援以及其他鎖定選項。可以使用 EntityManager.lock() API 獲取鎖,或將 LockModeType 傳遞給 EntityManagerfind()refresh() 操作,或設定 QueryNamedQuerylockMode

JPA 2.0 鎖定模式在 LockModeType 列舉中定義

  • OPTIMISTIC(在 JPA 1.0 中為 READ)- 實體將在提交時檢查其樂觀鎖定版本,以確保沒有其他事務更新了該物件。
  • OPTIMISTIC_FORCE_INCREMENT(在 JPA 1.0 中為 WRITE)- 實體將在提交時增加其樂觀鎖定版本,以確保沒有其他事務更新(或讀取鎖定)該物件。
  • PESSIMISTIC_READ - 實體在資料庫中被鎖定,阻止任何其他事務獲取 PESSIMISTIC_WRITE 鎖。
  • PESSIMISTIC_WRITE - 實體在資料庫中被鎖定,阻止任何其他事務獲取 PESSIMISTIC_READ 或 PESSIMISTIC_WRITE 鎖。
  • PESSIMISTIC_FORCE_INCREMENT - 實體在資料庫中被鎖定,阻止任何其他事務獲取 PESSIMISTIC_READ 或 PESSIMISTIC_WRITE 鎖,並且實體將在提交時增加其樂觀鎖定版本。這很不尋常,因為它同時執行樂觀和悲觀鎖定,通常應用程式只使用一種鎖定模型。
  • NONE - 不獲取鎖,這是任何查詢、重新整理或查詢操作的預設值。

JPA 2.0 還添加了兩個新的標準查詢提示。這些可以傳遞給任何 QueryNamedQueryfind()lock()refresh() 操作。

  • "javax.persistence.lock.timeout" - 在放棄並丟擲 PessimisticLockException 之前等待鎖的毫秒數。
  • "javax.persistence.lock.scope" - 有效範圍在 PessimisticLockScope 中定義,可以是 NORMAL 或 EXTENDED。EXTENDED 還將鎖定物件的所屬連線表和元素集合表。

可序列化事務隔離

[編輯 | 編輯原始碼]

可序列化事務隔離保證在事務中讀取的任何內容都不會被任何其他使用者更新。透過使用可序列化事務隔離並確保在同一事務中讀取要編輯的資料,您可以實現悲觀鎖定。重要的是要確保在事務中從資料庫重新整理物件,因為編輯快取或可能過時的資料會破壞鎖定的目的。

可序列化事務隔離通常可以在資料庫中啟用,一些資料庫甚至將其作為預設值。它也可以在 JDBC Connection 上設定,或者透過本地 SQL 設定,但這特定於資料庫,不同的資料庫具有不同的支援級別。可序列化事務隔離的主要問題與使用 SELECT ... FOR UPDATE 相同(有關詳細資訊,請參見上文),此外,讀取的任何內容都將被鎖定,因此您無法決定只在某些時候鎖定某些物件,而是一直鎖定所有內容。對於具有公共只讀資料的交易來說,這可能是一個重大的併發問題,並可能導致死鎖。

資料庫如何實現可序列化事務隔離在不同的資料庫之間有所不同。一些資料庫(例如 Oracle)可以以比典型悲觀實現更樂觀的意義上執行可序列化事務隔離。不是每個事務都需要在讀取資料時對所有資料進行鎖定,而是直到事務提交時才檢查行版本,如果任何資料發生了更改,則會丟擲異常並且不允許事務提交。

華夏公益教科書