我正在尝试使用jpa和db作为mysql在我的spring启动应用程序中实现悲观锁定。我的目标是让存储库首先从db中获取一行,然后对此进行设置锁定。当这个事务运行时,没有人应该能够读取同一行。以下是我实施的代码:
@Repository
@Transactional
public class UserRepo {
@PersistenceContext
private EntityManager entityManager;
/**
*
* @param token
* @param data
* @return
*/
public boolean lockUser(String token, int data) {
Map<String, Object> props = new HashMap<String, Object>();
props.put("javax.persistence.query.timeout", 0);
User usr = entityManager.find(User.class, token, LockModeType.PESSIMISTIC_WRITE, props);
System.out.println("BEFOREE LOCK = " +
Thread.currentThread().getId() + " user="+usr.getPlayerBalance());
entityManager.lock(usr, LockModeType.PESSIMISTIC_WRITE, props);
System.out.println("AFTER LOCK = " + Thread.currentThread().getId());
if (data>2) {
System.out.println("IN IF BEFORE SLEEP Thread = " + Thread.currentThread().getId());
Thread.sleep(90000);
System.out.println("IN IF AFTER SLEEP Thread = " + Thread.currentThread().getId());
} else {
System.out.println("IN ELSE Thread = " + Thread.currentThread().getId());
} return false;
}
}
现在,当我运行此功能时,第一个请求带有数据&gt; 3,这个获取行然后锁定行和线程休眠90秒。现在当第二个请求带有data = 1时,线程等待锁定(em.find-具有悲观锁定,超时为0毫秒)。理想情况下,它应该抛出异常,因为我已将超时设置为0.但是第二个线程不会立即抛出expcetion,而且线程从db读取行然后等待。
答案 0 :(得分:1)
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");
}
}
我为该回购创建了一个(不漂亮但功能齐全的)测试:
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
}
}
如果我们现在假设有一个类型为User的实体,其id为1(Long),则输出为:
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%为真,还是我们使用其他一些技巧)。
所以你不应该依赖超时例外。