代码之家  ›  专栏  ›  技术社区  ›  damian

乐观锁定不工作的Spring数据JPA

  •  0
  • damian  · 技术社区  · 6 年前

    我无法在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> {
    }
    

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

    插入用户(用户名、名字、姓氏、电子邮件、版本)

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

    顺便说一下,我试图重写存储库中的save方法来添加@Lock(LockModeType.com)但一切都没有改变。

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

    使用这段代码,我总是得到乐观锁异常,因为插入不是同时执行的。没有这种丑陋的解决办法,我从来都不是例外。有办法解决吗?(此解决方案除外)。或者我不应该担心生产中发生的这种情况?

    0 回复  |  直到 6 年前
        1
  •  -2
  •   Cristian Colorado    6 年前

    你可以利用 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 我们可以从中恢复过来 执行服务

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