首页 > 解决方案 > 当更新同时运行时,乐观锁定不工作 Spring Data JPA

问题描述

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

(请看底部的编辑)

这是我的实体:

@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> {
}

这是我的服务:

@Service
public class UserService {

  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public User updateUser(User user)
        throws UserNotFoundException {
    final Optional<User> oldUserOpt =  userRepository.findById(user.getId());
    User oldUser = oldUserOpt
            .orElseThrow(UserNotFoundException::new);

        logger.debug("udpateUser(): saving user. {}", user.toString());
        oldUser.setFistName(user.getFistName());
        oldUser.setLastName(user.getLastName());
        oldUser.setEmail(user.getEmail());
        return userRepository.save(oldUser);        
  }
}

最后这是我的测试:

@SpringBootTest
@AutoConfigureMockMvc
@RunWith(SpringRunner.class)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class UserControllerIntegrationTest {

  @Test
  public void testConcurrentUpdate() throws Exception {

    String body1 = "{\"fistName\":\"John\",\"lastName\":\"Doe\",\"email\":\"johno@gmail.com\"}";
    String body2 = "{\"fistName\":\"John\",\"lastName\":\"Watkins\",\"email\":\"johno@gmail.com\"}";
    
    Runnable runnable1 = () -> {
        try {
            mvc.perform(put("/v1/users/1")
                    .contentType(MediaType.APPLICATION_JSON)
                    .characterEncoding("UTF-8")
                    .content(body1));
        } catch (Exception e) {
            System.out.println("exception in put " + e);
        }
    };

    Runnable runnable2 = () -> {
        try {
            mvc.perform(put("/v1/users/1")
                    .contentType(MediaType.APPLICATION_JSON)
                    .characterEncoding("UTF-8")
                    .content(body2));
        } catch (Exception e) {
            System.out.println("exception in put " + e);
        }
    };

    Thread t1 = new Thread(runnable1);
    Thread t2 = new Thread(runnable2);

    t1.start();
    t2.start();
    t1.join();
    t2.join();

    System.out.println("user after updates: " + userRepository.findById(1).get().toString());
  }
}

当测试运行时,数据库中只有这条记录(使用内存中的 h2):插入用户(用户 ID,名字,姓氏,电子邮件,版本)值(1,'John','Oliver','johno@gmail. com', 1);

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

顺便说一句,我尝试覆盖存储库中的保存方法以添加 @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() 之前,我在服务中添加了一些此代码:

double random = Math.random();
long wait = (long) (random * 500);
logger.debug("waiting {} ms", wait);
try {
    Thread.sleep(wait);
} catch (InterruptedException e) {
    e.printStackTrace();
}

使用这段代码,我总是得到 Optimistic Lock 异常,因为插入不是同时执行的。如果没有这种丑陋的解决方法,我永远不会遇到异常。有没有办法解决这个问题?(除了这个解决方法)。或者我不应该担心生产中发生这种情况?

标签: javaspringhibernatespring-bootspring-data-jpa

解决方案


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

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

  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());
  }

推荐阅读