为了账号安全,请及时绑定邮箱和手机立即绑定

当更新同时运行时,乐观锁定不工作 Spring Data JPA

当更新同时运行时,乐观锁定不工作 Spring Data JPA

蝴蝶不菲 2022-07-20 16:10:03
我无法使用 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> {}这是我的服务:@Servicepublic 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\"}";}当测试运行时,数据库中只有这条记录(使用内存中的 h2):插入用户(用户 ID,名字,姓氏,电子邮件,版本)值(1,'John','Oliver','johno@gmail. com', 1);这些是日志。我注意到正在检查并在 sql 中设置版本,所以工作正常。事务结束时执行update语句,但两个事务都执行成功,无异常。顺便说一句,我尝试覆盖存储库中的保存方法以添加 @Lock(LockModeType.OPTIMISTIC) 但没有任何改变。
查看完整描述

2 回答

?
慕斯709654

TA贡献1840条经验 获得超5个赞

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


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


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

  }


查看完整回答
反对 回复 2022-07-20
?
汪汪一只猫

TA贡献1898条经验 获得超8个赞

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


我做了一个打电话给你的UserService班级的例子:


存储库


public interface UserRepository extends CrudRepository<User, Long> {


  @Lock(value = LockModeType.OPTIMISTIC)

  Optional<User> findById(Long 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();

      }

    }

  }

我们使用Callable,因为它可以抛出Exceptions,我们可以从中恢复ExecutorService。


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


查看完整回答
反对 回复 2022-07-20
  • 2 回答
  • 0 关注
  • 112 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信