網頁

2020/12/7

Spring Data JPA 樂觀鎖 optimistic locking

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的屬性型態可以是intIntegershortShortlongLong, 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


沒有留言:

張貼留言