背景介紹
@Version是jpa里提供的一個注解,其作用是用於實現樂觀鎖。在JPA的幫助下實現樂觀鎖十分簡單,只需將我們的一個java的entity加上一個由@version修飾的字段即可。然后我們每次去對這個entity進行更新操作的時候,JPA就會去比較這個version並且在操作成功之后自動更新它,若version與當前數據庫的不匹配,則更新操作失敗並拋出下面這個異常javax.persistence.OptimisticLockException。
下面是一個使用注解@Version的entity的例子代碼
@Entity
@Table(name = "PRODUCT")
public class Product {
@Id
@Column(name = "ID")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
@Column(name = "NAME")
private String name;
@Version
@Column(name = "VERSION")
private Integer version;
...
}
在這個例子當中,我們定義了一個Integer類型的version作為用於檢測的字段。這是比較常見的方式,還有一種方式是定義一個Date類型的字段作為檢測的字段,使用哪種類型通常取決於所使用的jpa實現以及每個應用程序的實際情況。在JPA的幫助下,我們只需要按上述定義我們的Entity,就能實現樂觀鎖的效果。
那么JPA是如何實現樂觀鎖的呢?這是本篇博客主要想要探究的問題。
什么是JPA
在介紹樂觀鎖之前我們不妨先來看一下JPA是個什么樣的東西。JPA全稱是Java Persistence API,這是一套用來持久化存儲數據到數據庫的類和方法的集合。JPA主要是為了減輕程序員為關系對象編寫代碼的負擔,使用JPA框架允許與數據庫實例輕松交互。由於JPA本身只是一套開源的API,因此很多企業都有對它進行自己的實現,我們比較常見的包括Hibernate, Eclipselink, Toplink, Spring Data JPA等等。知道了JPA的作用之后,我們就更好理解為什么它會來實現數據更新的樂觀鎖的相關功能了。
什么是樂觀鎖?使用場景是什么?
樂觀鎖即Optimistic lock本質上是一個用來防止更新丟失的機制。所謂更新丟失指的是一種用戶並發操作下可能會發生的場景。假設我們現在有兩個用戶,他們都有操作相同數據的權限,這兩個用戶現在都拿到了當前數據庫最新的數據。這時其中一個用戶更改了一部分數據,保存進了數據庫,另一個用戶又更改了另一部分數據並保存進了數據庫,那么第二個用戶的操作就會把第一個用戶更新的數據沖掉。
這就是樂觀鎖要避免的場景。所謂樂觀鎖,就是指上面的第二個用戶是可以編輯它已經過時的數據並保存的,只是最后保存的時候他會得到一個異常。
樂觀鎖的效果如下圖所示:
用戶A和用戶B同時對version為1的數據進行操作,那么只有先更新的用戶能操作成功,后更新的用戶則會因為數據不是最新的而更新失敗,如下所示:
A用戶先更新會成功,而B用戶則會失敗,拋出樂觀鎖異常,這就是樂觀鎖的效果.
JPA樂觀鎖的實現原理
我們在上面已經提到了JPA是通過在一個數據庫表對應的Entity當中添加一個特殊的字段來使用樂觀鎖的。在添加這個特殊字段之后,數據庫表里面的每一行記錄也就多了這么一個字段,事實上這本質是是數據庫的一種行級鎖。因此我們完全可以先拋開樂觀鎖來看一下數據庫的行級鎖。
數據庫的行級鎖
針對以上這個數據庫表,如果有兩個不同的事務嘗試更新同一條記錄,則后一個執行修改更新語句的事務將被會一直被鎖定到第一個事務完成其工作(第一個事務有可能是提交或回滾)。例如:
事務一:
-
BEGIN;
-
UPDATE PRODUCT
-
SET NAME = "Car new"
-
WHERE ID = 1;
-
COMMIT;
事務二:
-
BEGIN;
-
UPDATE PRODUCT
-
SET NAME = "Car new 1"
-
WHERE ID = 1;
-
COMMIT;如果事務一和二碰巧在執行期間同時執行上述各個語句,則其中一個語句肯定會在該更新語句上被鎖定,直到另一個事務完成,這一點是由數據庫保證的,這也是數據庫的基本職責之一。 其原因就是他們正在更新相同的記錄,即主鍵都為ID = 1的記錄。 數據庫將鎖定該記錄的所有修改,直到持有鎖的事務完成其工作。
樂觀鎖
現在,有了上述知識之后,我們再來看樂觀鎖的實現就非常容易了。正如之前提到的一樣,樂觀鎖的實現是依賴於更新執行期間的數據比較。 這意味着我們需要一個可用於此比較的字段,或字段集合,比如我們在前面定義的version字段。下面我們就來看一下使用另一個名為version的字段的場景:
樂觀鎖的實現原理是,任何要更新某條記錄的事務必須首先讀取該記錄,以便知道當前version字段的值,在之后的update語句也需要用到該值。
假設現在有一個事務要更新上表中ID為1的記錄。那么首先它將讀取這條記錄,獲取當前這條記錄的version值,這個事務不僅需要將當前的version和它讀到的做校驗,同時還要在校驗成功之后將version值更新成另一個值,這里所謂的做校驗,就是將這個讀到的version值放當sql語句的where條件里面即可。下面是一個例子:
-
UPDATE PRODUCT
-
SET NAME = "new name", VERSION = 3
-
WHERE ID = 1 AND VERSION = 2;
而數據庫中有一個機制是在一條語句執行之后返回更新的數據的數量,那么在這條語句的事務執行之后,自然也會返回一個count,因此作為jpa的實現者可以通過count判定是否更新成功了,若count為1則代表執行成功,若count為0則表示更新失敗,而更新失敗則說明此時另一個事務已經更新了這條數據,jpa實現通常會返回一個OptimisticLockException.
以postgresql數據庫為例,執行一個更新一條記錄的語句成功之后,可以看到下面的message:
這里的one row affected,就表明更新的數據數量為1.
再看我們上面的例子,如果在這條語句執行時沒有其他事務同時更改了該記錄,則VERSION值仍然是2,where條件滿足,因此數據庫返回的更新的數據行數將是預期的:1。這樣,應用程序就能知道沒有其他並發更新發生,可以繼續安全地提交更改。
一句話總結起來:jpa實現更新時將version值置於sql語句的where條件當中,去嘗試更新(樂觀的),通過返回的更新條數判斷是否更新成功。
哪些數據類型可以作為樂觀鎖的判定條件
如果系統可以更改Integer,Long等類型,則使用這樣的字段通常是一個好的選擇。
我們也可以使用一個Date類型的變量來實現。但是如果極端的並發情況超越了我們數據庫的時間粒度,則這種鎖可能會fail
還有一種比較昂貴的實現方式則是把整個entity作為一個判定對象。
其他
樂觀鎖只適用於我們的系統由於某些業務需求而無法容忍丟失的更新現象,當然,也有許多系統丟失更新根本不是問題,因此樂觀鎖多他們來說並不適用。
在某些情況下,當version的更新與batch的操作一起使用時,可能會出現問題。有一個例子是Oracle JDBC驅動程序無法在JDBC批處理語句執行中提取正確數量的更新行計數。如果我們還遇到此問題,可以檢查是否已將Hibernate屬性hibernate.jdbc.batch_versioned_data設置為true。當此設置為true時,即使針對版本化數據進行更新,Hibernate也將使用批量更新。Hibernate中此設置的默認值為false,因此當它檢測到將在給定的刷新操作中執行版本化數據更新時,將不會使用批量更新。
此外我們不難看出樂觀鎖定實際上並不是真正的DB的鎖。 它只是通過比較版本列的值來工作。 並不會阻止其他進程訪問任何數據。