1. 程式人生 > 實用技巧 >Spring Data JPA註解@Version樂觀鎖是如何實現的

Spring Data JPA註解@Version樂觀鎖是如何實現的

背景介紹

@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當中新增一個特殊的欄位來使用樂觀鎖的。在新增這個特殊欄位之後,資料庫表裡面的每一行記錄也就多了這麼一個欄位,事實上這本質是是資料庫的一種行級鎖。因此我們完全可以先拋開樂觀鎖來看一下資料庫的行級鎖。

資料庫的行級鎖

針對以上這個資料庫表,如果有兩個不同的事務嘗試更新同一條記錄,則後一個執行修改更新語句的事務將被會一直被鎖定到第一個事務完成其工作(第一個事務有可能是提交或回滾)。例如:

事務一:

  1. BEGIN;
  2. UPDATE PRODUCT
  3. SET NAME = "Car new"
  4. WHERE ID = 1;
  5. COMMIT;

事務二:

  1. BEGIN;
  2. UPDATE PRODUCT
  3. SET NAME = "Car new 1"
  4. WHERE ID = 1;
  5. COMMIT; 如果事務一和二碰巧在執行期間同時執行上述各個語句,則其中一個語句肯定會在該更新語句上被鎖定,直到另一個事務完成,這一點是由資料庫保證的,這也是資料庫的基本職責之一。 其原因就是他們正在更新相同的記錄,即主鍵都為ID = 1的記錄。 資料庫將鎖定該記錄的所有修改,直到持有鎖的事務完成其工作。

樂觀鎖

現在,有了上述知識之後,我們再來看樂觀鎖的實現就非常容易了。正如之前提到的一樣,樂觀鎖的實現是依賴於更新執行期間的資料比較。 這意味著我們需要一個可用於此比較的欄位,或欄位集合,比如我們在前面定義的version欄位。下面我們就來看一下使用另一個名為version的欄位的場景:

樂觀鎖的實現原理是,任何要更新某條記錄的事務必須首先讀取該記錄,以便知道當前version欄位的值,在之後的update語句也需要用到該值。

假設現在有一個事務要更新上表中ID為1的記錄。那麼首先它將讀取這條記錄,獲取當前這條記錄的version值,這個事務不僅需要將當前的version和它讀到的做校驗,同時還要在校驗成功之後將version值更新成另一個值,這裡所謂的做校驗,就是將這個讀到的version值放當sql語句的where條件裡面即可。下面是一個例子:

    1. UPDATE PRODUCT
    2. SET NAME = "new name", VERSION = 3
    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的鎖。 它只是通過比較版本列的值來工作。 並不會阻止其他程序訪問任何資料。