Оптимистическая блокировка не работает Spring Data JPA, когда обновления выполняются в одно и то же время - PullRequest
0 голосов
/ 04 марта 2019

Я не могу получить оптимистическую блокировку для работы над проектом Spring Boot 2 с Spring Data JPA.У меня есть тест, который запускает 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 в памяти): вставить в User (UserID,Значения FirstName, LastName, Email, Version) (1, «Джон», «Оливер», «johno@gmail.com», 1);

Это журналы.Я заметил, что версия проверяется и устанавливается в SQL, так что работает нормально.Оператор обновления выполняется, когда транзакция заканчивается, но обе транзакции выполняются успешно, без исключения.

Кстати, я попытался переопределить метод сохранения в репозитории, чтобы добавить @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, потому что вставки не выполняются одновременно.Без этого уродливого обходного пути я никогда не был исключением.Есть ли способ решить это?(кроме этого обходного пути).Или мне не стоит беспокоиться об этом сценарии, происходящем в производстве?

1 Ответ

0 голосов
/ 04 марта 2019

Вы можете использовать ExecutorService для управления многопоточностью и CyclicBarrier для синхронизации выполнения потоков (или, по крайней мере, сократить промежуток времени выполнения между потоками).

Я сделал пример, вызывая ваш UserService класс:

Репозиторий

public interface UserRepository extends CrudRepository<User, Long> {

  @Lock(value = LockModeType.OPTIMISTIC)
  Optional<User> findById(Long id);
}

JUnit Test Case

// 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.Поскольку вы будете вызывать контроллер, я бы предложил увеличить количество потоков для повышения шансов.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...