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