Java 当更新同时运行时,乐观锁定不工作的Spring数据JPA
我无法使用SpringDataJPA在SpringBoot2项目上进行乐观锁定。我有一个测试,在不同的线程中运行两个简单的更新,但是它们都成功(没有乐观锁异常),并且其中一个更新被另一个更新覆盖 (请查看底部的编辑) 这是我的实体:Java 当更新同时运行时,乐观锁定不工作的Spring数据JPA,java,spring,hibernate,spring-boot,spring-data-jpa,Java,Spring,Hibernate,Spring Boot,Spring Data Jpa,我无法使用SpringDataJPA在SpringBoot2项目上进行乐观锁定。我有一个测试,在不同的线程中运行两个简单的更新,但是它们都成功(没有乐观锁异常),并且其中一个更新被另一个更新覆盖 (请查看底部的编辑) 这是我的实体: @Entity @Table(name = "User") public class User { @Column(name = "UserID") @Id @GeneratedValue(strategy
@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> {
}
当测试运行时,DB中只有该记录(在内存中使用h2):
插入到用户中(用户名、姓氏、姓氏、电子邮件、版本)
值(1,'John','Oliver','johno@gmail.com', 1);
这些是日志。我注意到sql中正在检查和设置版本,因此工作正常。update语句在事务结束时执行,但两个事务都成功执行,无异常
顺便说一句,我尝试覆盖存储库中的save方法来添加@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()之前,我在服务中添加了一些此代码:
在这段代码中,我总是得到乐观锁异常,因为插入不是同时执行的。如果没有这种难看的解决办法,我永远也不会例外。有办法解决这个问题吗?(此解决方法除外)。或者我不应该担心这种情况在生产中发生?您可以使用
ExecutorService
来管理多线程和CyclicBarrier
,以便同步线程执行(或者至少缩短线程之间的执行时间间隔)
我做了一个调用UserService
类的示例:
存储库
public interface UserRepository extends CrudRepository<User, Long> {
@Lock(value = LockModeType.OPTIMISTIC)
Optional<User> findById(Long id);
}
public interface UserRepository扩展了crudepository{
@锁定(值=LockModeType.OPTIMISTIC)
可选的findById(长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();
}
}
}
//创建更新用户的可调用
公共可调用createCallable(用户用户、整数、循环传送门)抛出OptimisticLockingFailureException{
返回()->{
//创建要更新的POJO时,我们向字符串字段添加一个数字
User newUser=新用户(User.getId(),
user.getFistName(),
user.getLastName()+“[”+tNumber+“]”,
user.getEmail());
//一直保持,直到所有线程都创建了POJO
门。等待();
//一旦CyclicBarrier打开,我们将运行更新
User updateUser=userService.updateUser(newUser);
返回true;
};
}
@测试(预期=ObjectOptimisticLockingFailureException.class)
public void userServiceShouldThrowOptimisticLockException()引发Throwable{
final int threads=2;//我们需要2个线程
final CyclicBarrier gate=新的CyclicBarrier(线程);//一旦有2个线程等待,屏障将打开
ExecutorService executor=Executors.newFixedThreadPool(线程);
//创建用于测试的用户
用户用户=新用户(“阿方索”、“卡隆”alfonso@ac.com");
User savedUser=userRepository.save(用户);
//创建N个调用服务的线程
列表任务=新建ArrayList();
对于(int i=0;i
我们使用Callable
,因为它可以抛出异常
,我们可以从ExecutorService
中恢复
注意,线程调用和save语句之间的指令越多,它们不同步导致OptimisticLockException的可能性就越大。由于您将调用控制器,我建议增加线程数量以获得更好的机会 乐观锁定可确保在加载和保存实体之间不会对其进行任何其他更改。由于您的服务在保存实体之前就加载了该实体,因此在这短时间内,另一个线程不太可能干预,这就是为什么只有在您使线程休眠时,您才会看到冲突 如果要将乐观锁定提供的保护扩展到数据库事务之外,可以将以前加载的实体传递给客户端并返回,然后保存它,而无需再次加载:
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());
}
谢谢Cristian,这不是内部错误,而是对并发更新的处理。我正在处理异常并将正确的消息返回给用户,但在本例中,我正在测试api是否可以处理同一resourceHi@Damian的并发更新。我确实使用此代码复制了错误。也许你能适应它。让我知道你的想法。谢谢你的回答,但这似乎更多的是一个解决办法,使测试工作,而不是实际解决问题。在生产过程中,该服务将被调用,而我将不会管理ThreadsWave您找到任何解决方案了吗,我的朋友?在更新a行@damianHi@meriton时,我也遇到了完全相同的问题,我已经从repo读取了用户,然后调用userRepository.save(oldUser);所以我不确定你提出的解决方案是什么,你能澄清一下吗?两个线程似乎都在同时读取记录(在另一个线程保存更改之前),并且都有正确的版本,所以第二个代码段不起作用
// 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();
}
}
}
public User updateUser(User user) {
return userRepository.save(user);
}
public User updateUser(UserDto user) {
User savedUser = userRepository.findById(user.getId());
if (savedUser.getVersion() != user.getVersion()) {
throw new OptimisticLockingViolationException();
}
savedUser.setName(user.getName());
}