Java 当更新同时运行时,乐观锁定不工作的Spring数据JPA

Java 当更新同时运行时,乐观锁定不工作的Spring数据JPA,java,spring,hibernate,spring-boot,spring-data-jpa,Java,Spring,Hibernate,Spring Boot,Spring Data Jpa,我无法使用SpringDataJPA在SpringBoot2项目上进行乐观锁定。我有一个测试,在不同的线程中运行两个简单的更新,但是它们都成功(没有乐观锁异常),并且其中一个更新被另一个更新覆盖 (请查看底部的编辑) 这是我的实体: @Entity @Table(name = "User") public class User { @Column(name = "UserID") @Id @GeneratedValue(strategy

我无法使用SpringDataJPA在SpringBoot2项目上进行乐观锁定。我有一个测试,在不同的线程中运行两个简单的更新,但是它们都成功(没有乐观锁异常),并且其中一个更新被另一个更新覆盖

(请查看底部的编辑)

这是我的实体:

@Entity
@Table(name = "User")
public class User {
  
  @Column(name = "UserID")
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Integer id;
  @Column(name = "FirstName")
  @NotBlank()
  private String fistName;
  @Column(name = "LastName")
  @NotBlank
  private String lastName;
  @Column(name = "Email")
  @NotBlank
  @Email
  private String email;
  @Version
  @Column(name = "Version")
  private long version;

  // getters & setters
}
这是我的存储库:

public interface UserRepository extends JpaRepository<User, Integer> {
}
当测试运行时,DB中只有该记录(在内存中使用h2): 插入到用户中(用户名、姓氏、姓氏、电子邮件、版本) 值(1,'John','Oliver','johno@gmail.com', 1);

这些是日志。我注意到sql中正在检查和设置版本,因此工作正常。update语句在事务结束时执行,但两个事务都成功执行,无异常

顺便说一句,我尝试覆盖存储库中的save方法来添加@Lock(LockModeType.OPTIMISTIC),但没有任何改变

[       Thread-4] c.u.i.service.UserService     : updateUser(): saving user. User{id=1, fistName='John', lastName='Doe', email='johno@gmail.com', version=1}
[       Thread-5] c.u.i.service.UserService     : updateUser(): saving user. User{id=1, fistName='John', lastName='Watkins', email='johno@gmail.com', version=1}
[       Thread-5] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.company.app.service.UserService.updateUser]
[       Thread-4] o.s.t.i.TransactionInterceptor           : Getting transaction for [com.company.app.service.UserService.updateUser]
[       Thread-4] o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findById]
[       Thread-5] o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findById]
[       Thread-4] org.hibernate.SQL                        : select user0_.UserID as Use1_3_0_, user0_.Email as Email2_3_0_, user0_.FirstName as FirstNam4_3_0_, user0_.LastName as LastName5_3_0_, user0_.Version as Version9_3_0_ from User user0_ where user0_.UserID=1
[       Thread-5] org.hibernate.SQL                        : select user0_.UserID as Use1_3_0_, user0_.Email as Email2_3_0_, user0_.FirstName as FirstNam4_3_0_, user0_.LastName as LastName5_3_0_, user0_.Version as Version9_3_0_ from User user0_ where user0_.UserID=1
[       Thread-5] o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findById]
[       Thread-4] o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findById]
[       Thread-5] o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
[       Thread-4] o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
[       Thread-4] o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
[       Thread-5] o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]
[       Thread-4] o.s.t.i.TransactionInterceptor           : Completing transaction for [com.company.app.service.UserService.updateUser]
[       Thread-5] o.s.t.i.TransactionInterceptor           : Completing transaction for [com.company.app.service.UserService.updateUser]
[       Thread-5] org.hibernate.SQL                        : update User set Email=johno@gmail.com, FirstName=John, LastName=Watkins, Version=2 where UserID=1 and Version=1
[       Thread-4] org.hibernate.SQL                        : update User set Email=johno@gmail.com, FirstName=John, LastName=Doe, Version=2 where UserID=1 and Version=1
user after updates: User{id=1, fistName='John', lastName='Watkins', email='johno@gmail.com', version=2}
编辑:

我认为问题在于插入是在同一时间完成的。 在调用save()之前,我在服务中添加了一些此代码:


在这段代码中,我总是得到乐观锁异常,因为插入不是同时执行的。如果没有这种难看的解决办法,我永远也不会例外。有办法解决这个问题吗?(此解决方法除外)。或者我不应该担心这种情况在生产中发生?

您可以使用
ExecutorService
来管理多线程和
CyclicBarrier
,以便同步线程执行(或者至少缩短线程之间的执行时间间隔)

我做了一个调用
UserService
类的示例:

存储库

public interface UserRepository extends CrudRepository<User, Long> {

  @Lock(value = LockModeType.OPTIMISTIC)
  Optional<User> findById(Long id);
}
public interface UserRepository扩展了crudepository{
@锁定(值=LockModeType.OPTIMISTIC)
可选的findById(长id);
}
JUnit测试用例

// Create a Callable that updates the user
  public Callable<Boolean> createCallable(User user, int tNumber, CyclicBarrier gate) throws OptimisticLockingFailureException {
    return () -> {
      // Create POJO to update, we add a number to string fields
      User newUser = new User(user.getId(),
              user.getFistName() + "[" + tNumber + "]",
              user.getLastName()  + "[" + tNumber + "]",
              user.getEmail());

      // Hold on until all threads have created POJO
      gate.await();

      // Once CyclicBarrier is open, we run update
      User updatedUser = userService.updateUser(newUser);

      return true;
    };
  }

  @Test(expected = ObjectOptimisticLockingFailureException.class)
  public void userServiceShouldThrowOptimisticLockException() throws Throwable {
    final int threads = 2; // We need 2 threads
    final CyclicBarrier gate = new CyclicBarrier(threads); // Barrier will open once 2 threads are awaiting
    ExecutorService executor = Executors.newFixedThreadPool(threads);

    // Create user for testing
    User user = new User("Alfonso", "Cuaron", "alfonso@ac.com");
    User savedUser = userRepository.save(user);

    // Create N threads that calls to service
    List<Callable<Boolean>> tasks = new ArrayList<>();
    for(int i = 0; i < threads; i++) {
      tasks.add(createCallable(savedUser, i, gate));
    }

    // Invoke N threads
    List<Future<Boolean>> result = executor.invokeAll(tasks);

    // Iterate over the execution results to browse exceptions
    for (Future<Boolean> r : result) {
      try {
        Boolean temp = r.get();
        System.out.println("returned " + temp);
      } catch (ExecutionException e) {
        // Re-throw the exception that ExecutorService catch
        throw e.getCause();
      }
    }
  }
//创建更新用户的可调用
公共可调用createCallable(用户用户、整数、循环传送门)抛出OptimisticLockingFailureException{
返回()->{
//创建要更新的POJO时,我们向字符串字段添加一个数字
User newUser=新用户(User.getId(),
user.getFistName(),
user.getLastName()+“[”+tNumber+“]”,
user.getEmail());
//一直保持,直到所有线程都创建了POJO
门。等待();
//一旦CyclicBarrier打开,我们将运行更新
User updateUser=userService.updateUser(newUser);
返回true;
};
}
@测试(预期=ObjectOptimisticLockingFailureException.class)
public void userServiceShouldThrowOptimisticLockException()引发Throwable{
final int threads=2;//我们需要2个线程
final CyclicBarrier gate=新的CyclicBarrier(线程);//一旦有2个线程等待,屏障将打开
ExecutorService executor=Executors.newFixedThreadPool(线程);
//创建用于测试的用户
用户用户=新用户(“阿方索”、“卡隆”alfonso@ac.com");
User savedUser=userRepository.save(用户);
//创建N个调用服务的线程
列表任务=新建ArrayList();
对于(int i=0;i
我们使用
Callable
,因为它可以抛出
异常
,我们可以从
ExecutorService
中恢复


注意,线程调用和save语句之间的指令越多,它们不同步导致OptimisticLockException的可能性就越大。由于您将调用控制器,我建议增加线程数量以获得更好的机会

乐观锁定可确保在加载和保存实体之间不会对其进行任何其他更改。由于您的服务在保存实体之前就加载了该实体,因此在这短时间内,另一个线程不太可能干预,这就是为什么只有在您使线程休眠时,您才会看到冲突

如果要将乐观锁定提供的保护扩展到数据库事务之外,可以将以前加载的实体传递给客户端并返回,然后保存它,而无需再次加载:

  public User updateUser(User user) {
      return userRepository.save(user);
  }
(这会调用
entityManager.merge()
,它会自动检查版本)

或者,如果您需要更细粒度地控制哪些字段被更新,您可以只传递这些字段和版本,并在保存时自己检查版本:

  public User updateUser(UserDto user) {
      User savedUser = userRepository.findById(user.getId());
      if (savedUser.getVersion() != user.getVersion()) {
          throw new OptimisticLockingViolationException();
      }
      savedUser.setName(user.getName());
  }

谢谢Cristian,这不是内部错误,而是对并发更新的处理。我正在处理异常并将正确的消息返回给用户,但在本例中,我正在测试api是否可以处理同一resourceHi@Damian的并发更新。我确实使用此代码复制了错误。也许你能适应它。让我知道你的想法。谢谢你的回答,但这似乎更多的是一个解决办法,使测试工作,而不是实际解决问题。在生产过程中,该服务将被调用,而我将不会管理ThreadsWave您找到任何解决方案了吗,我的朋友?在更新a行@damianHi@meriton时,我也遇到了完全相同的问题,我已经从repo读取了用户,然后调用userRepository.save(oldUser);所以我不确定你提出的解决方案是什么,你能澄清一下吗?两个线程似乎都在同时读取记录(在另一个线程保存更改之前),并且都有正确的版本,所以第二个代码段不起作用
// Create a Callable that updates the user
  public Callable<Boolean> createCallable(User user, int tNumber, CyclicBarrier gate) throws OptimisticLockingFailureException {
    return () -> {
      // Create POJO to update, we add a number to string fields
      User newUser = new User(user.getId(),
              user.getFistName() + "[" + tNumber + "]",
              user.getLastName()  + "[" + tNumber + "]",
              user.getEmail());

      // Hold on until all threads have created POJO
      gate.await();

      // Once CyclicBarrier is open, we run update
      User updatedUser = userService.updateUser(newUser);

      return true;
    };
  }

  @Test(expected = ObjectOptimisticLockingFailureException.class)
  public void userServiceShouldThrowOptimisticLockException() throws Throwable {
    final int threads = 2; // We need 2 threads
    final CyclicBarrier gate = new CyclicBarrier(threads); // Barrier will open once 2 threads are awaiting
    ExecutorService executor = Executors.newFixedThreadPool(threads);

    // Create user for testing
    User user = new User("Alfonso", "Cuaron", "alfonso@ac.com");
    User savedUser = userRepository.save(user);

    // Create N threads that calls to service
    List<Callable<Boolean>> tasks = new ArrayList<>();
    for(int i = 0; i < threads; i++) {
      tasks.add(createCallable(savedUser, i, gate));
    }

    // Invoke N threads
    List<Future<Boolean>> result = executor.invokeAll(tasks);

    // Iterate over the execution results to browse exceptions
    for (Future<Boolean> r : result) {
      try {
        Boolean temp = r.get();
        System.out.println("returned " + temp);
      } catch (ExecutionException e) {
        // Re-throw the exception that ExecutorService catch
        throw e.getCause();
      }
    }
  }
  public User updateUser(User user) {
      return userRepository.save(user);
  }
  public User updateUser(UserDto user) {
      User savedUser = userRepository.findById(user.getId());
      if (savedUser.getVersion() != user.getVersion()) {
          throw new OptimisticLockingViolationException();
      }
      savedUser.setName(user.getName());
  }