LockModeType.PESSIMISTIC_WRITE 用于锁定行,可以轻松测试。
我将 UserRepo 稍微调整为:
@Repository
public class UserRepo {
@PersistenceContext
private EntityManager entityManager;
@Transactional
public void lockUser(final Long id, final boolean wait) throws InterruptedException {
entityManager.clear(); // be sure there is nothing in the cache, actually the threads don't share first level cache
final Map<String, Object> props = new HashMap<String, Object>();
props.put("javax.persistence.query.timeout", 0);
System.out.println("Thread " + Thread.currentThread().getId() + " EXECUTES SELECT FOR UPDATE");
entityManager.find(User.class, id, LockModeType.PESSIMISTIC_WRITE, props);
if (wait) {
System.out.println("Thread " + Thread.currentThread().getId() + " started blocking!");
Thread.sleep(10000);
System.out.println("Thread " + Thread.currentThread().getId() + " finished blocking!");
}
System.out.println("Thread " + Thread.currentThread().getId() + " FINISHED QUERY");
}
}
我为那个 repo 创建了一个(不是漂亮但实用的)测试:
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;
@RunWith(SpringRunner.class)
@Transactional
@SpringBootTest
public class UserRepoTests {
@Autowired
private UserRepo userRepo;
@Test
public void testSelectForUpdate() throws InterruptedException {
final Runnable requestOne = () -> {
try {
userRepo.lockUser(1L, true); // this one should wait and block the others
} catch (InterruptedException e) {
}
};
final Runnable requestTwo = () -> {
try {
userRepo.lockUser(1L, false);
} catch (InterruptedException e) {
}
};
final Runnable requestThree = () -> {
try {
userRepo.lockUser(1L, false);
} catch (InterruptedException e) {
}
};
final Thread threadOne = new Thread(requestOne);
threadOne.start();
Thread.sleep(1000); // give the first one some time to start
final Thread threadTwo = new Thread(requestTwo);
threadTwo.start();
final Thread threadThree = new Thread(requestThree);
threadThree.start();
Thread.sleep(20000); // wait before destroying context
}
}
如果我们现在假设有一个 ID 为 1(Long)的 User 类型的实体,则输出为:
Thread 16 EXECUTES SELECT FOR UPDATE
Hibernate: select user0_.id as id1_31_0_, user0_.player_balance as player_b2_31_0_ from "user" user0_ where user0_.id=? for update
Thread 16 started blocking!
Thread 17 EXECUTES SELECT FOR UPDATE
Hibernate: select user0_.id as id1_31_0_, user0_.player_balance as player_b2_31_0_ from "user" user0_ where user0_.id=? for update
Thread 18 EXECUTES SELECT FOR UPDATE
Hibernate: select user0_.id as id1_31_0_, user0_.player_balance as player_b2_31_0_ from "user" user0_ where user0_.id=? for update
Thread 16 finished blocking!
Thread 16 FINISHED QUERY
Thread 17 FINISHED QUERY
Thread 18 FINISHED QUERY
因此,在调用entityManager.find(... LockModeType.PESSIMISTIC_WRITE...); 之后,此查询的所有后续执行都会等待第一个(因为SELECT ... FOR UPDATE),因此不需要entityManager.lock(...) 调用。
丢失的异常可能是由于查询超时只是一个提示,您的数据库可能没有考虑到这一事实。见the docs。
QueryTimeoutException:查询花费的时间超过指定的超时时间(请参阅 javax.persistence.query.timeout - 此属性是一个提示,可能不会被遵循)
或者也在同一页面上:
javax.persistence.query.timeout 查询超时,以毫秒为单位(整数或字符串),这是 Hibernate 使用的提示,但需要底层数据库的支持(TODO 是 100% 正确,还是我们使用其他技巧)。
所以你不应该依赖超时异常。