Spring Data JPA 樂觀鎖(Optimistic locking)範例如下。
範例環境:
- Spring Boot 2.3.2.RELEASE
- Spring Data JPA
- H2 Database
- JUnit 5
Spring Data JPA 可透過在Entity類新增一版本屬性並加上@Version
將該欄位標註為樂觀鎖判斷資料是否已被其他交易更新的版號。
例如下面是加上樂觀鎖版號的Entity類Employee
,在屬性version
上標注了@Version
。
Employee
package com.abc.demo.entity;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Version;
import java.io.Serializable;
@Entity
public class Employee implements Serializable {
private static final Long serialVersionUID = 1L;
@Id
private long id;
private String name;
@Version
private long version;
// getters and setters
}
標註@Version
的屬性型態可以是int
、Integer
、short
、Short
、long
、Long
, java.sql.Timestamp
。Spring Data JPA在更新entity時會自動遞增值。
EmployeeService.updateName()
先讀取然後更新名稱,並捕捉更新時可能因樂觀鎖的版號過時而丟出的OptimisticLockingFailureException
。
EmployeeService.updateNameUntilSuccess()
中當樂觀鎖造成的例外發生時,利用while loop持續重試更新直到成功為止。
EmployeeService
package com.abc.demo.service;
import com.abc.demo.entity.Employee;
import com.abc.demo.repository.EmployeeRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.stereotype.Service;
import javax.persistence.EntityNotFoundException;
@Service
public class EmployeeService {
@Autowired
private EmployeeRepository employeeRepository;
public void updateName(long id, String name) {
try {
System.out.println(Thread.currentThread().getName() + " update begin");
Employee employee = employeeRepository.findById(id)
.orElseThrow(EntityNotFoundException::new);
employee.setName(name);
employeeRepository.save(employee);
System.out.println(Thread.currentThread().getName() + " update success");
} catch (OptimisticLockingFailureException e) {
System.out.println("Optimistic locking occur");
e.printStackTrace();
}
}
public void updateNameUntilSuccess(long id, String name) {
boolean success = false;
while (!success) {
try {
System.out.println(Thread.currentThread().getName() + " update begin");
Employee employee = employeeRepository.findById(id)
.orElseThrow(EntityNotFoundException::new);
employee.setName(name);
employeeRepository.save(employee);
System.out.println(Thread.currentThread().getName() + " update success");
success = true;
} catch (OptimisticLockingFailureException e) {
System.out.println("Optimistic locking occur");
try {
Thread.sleep(1000L); // wait 1 seconds
} catch (InterruptedException interruptedException) {
System.out.println("Thread interrupted");
}
}
}
}
}
使用EmployeeServiceTests
測試。
第一個updateName_accumulateVersion()
測試版號是否有因為更新而自動累加。
第二個updateName_concurrentUpdateOptimisticLocking()
使用ExecutorService
執行兩條執行緒同時進行兩筆併發交易來測試樂觀鎖的作用。
第三個updateNameUntilSuccess_success
測試兩筆併發交易都成功更新。
EmployeeServiceTests
package com.abc.demo.service;
import com.abc.demo.entity.Employee;
import com.abc.demo.repository.EmployeeRepository;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import javax.persistence.EntityNotFoundException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@SpringBootTest
public class EmployeeServiceTests {
@Autowired
private EmployeeService employeeService;
@Autowired
private EmployeeRepository employeeRepository;
@Test
void updateName_accumulateVersion() {
long id = 1L;
Employee employee = new Employee();
employee.setId(id);
employee.setName("John");
employeeRepository.save(employee);
String newName = "Luke";
employeeService.updateName(id, newName);
employee = employeeRepository.findById(id).orElseThrow(EntityNotFoundException::new);
Assertions.assertEquals(newName, employee.getName());
Assertions.assertEquals(1L, employee.getVersion());
}
@Test
void updateName_concurrentUpdateOptimisticLocking() throws InterruptedException {
long id = 1L;
Employee employee = new Employee();
employee.setId(id);
employee.setName("John");
employeeRepository.save(employee);
final ExecutorService executorService = Executors.newFixedThreadPool(2); // create 2 threads in thread pool
// transaction 1
executorService.execute(() -> employeeService.updateName(id, "Luke"));
// transaction 2
executorService.execute(() -> employeeService.updateName(id, "Leon"));
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.MINUTES);
Employee result = employeeRepository.findById(1L)
.orElseThrow(EntityNotFoundException::new);
Assertions.assertEquals(1L, result.getVersion());
}
@Test
void updateNameUntilSuccess_success() throws InterruptedException {
long id = 1L;
Employee employee = new Employee();
employee.setId(id);
employee.setName("John");
employeeRepository.save(employee);
final ExecutorService executorService = Executors.newFixedThreadPool(2); // create 2 threads in thread pool
// transaction 1
executorService.execute(() -> employeeService.updateNameUntilSuccess(id, "Luke"));
// transaction 2
executorService.execute(() -> employeeService.updateNameUntilSuccess(id, "Leon"));
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.MINUTES);
Employee result = employeeRepository.findById(1L)
.orElseThrow(EntityNotFoundException::new);
Assertions.assertEquals(2L, result.getVersion());
}
}
執行updateName_concurrentUpdateOptimisticLocking()
會丟出以下錯誤訊息。
org.springframework.orm.ObjectOptimisticLockingFailureException: Object of class [com.abc.demo.entity.Employee] with identifier [1]: optimistic locking failed; nested exception is org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.abc.demo.entity.Employee#1]
...
Caused by: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.abc.demo.entity.Employee#1]
...
執行updateNameUntilSuccess_success()
印出以下訊息,兩筆交易都成功更新。
pool-2-thread-1 update begin
pool-2-thread-2 update begin
pool-2-thread-1 update success
Optimistic locking occur
pool-2-thread-2 update begin
pool-2-thread-2 update success
參考github。
沒有留言:
張貼留言