【发布时间】:2019-04-19 12:53:44
【问题描述】:
我使用 Hibernate 5 和 Oracle 12。 通过以下查询,我想从一组实体中随机选择一个实体:
Query query = getSession().createQuery("SELECT e FROM Entity e ... <CONDITIONS> ... AND ROWNUM = 1");
Optional<Entity> entity = query.list().stream().findAny();
// Change the entity in some way. The changes will also make sure that the entity won't appear in the next query run based on <CONDITIONS>
...
这可行,但前提是所有执行代码的事务都按顺序运行。因此,我还想确保已经读取的实体不会在另一个事务中被读取。 我尝试了锁定:
Query query = getSession().createQuery("SELECT e FROM Entity e ... <CONDITIONS> ... AND ROWNUM = 1")
.setLockMode("this", LockMode.PESSIMISTIC_READ);
但似乎 Hibernate 将此构造转换为 SELECT ... FOR UPDATE,这不会阻止其他事务读取实体,等待使用它的其他事务提交,然后在实体上应用它们自己的更改。
是否可以在实体上设置某种锁定,以保证它从另一个事务的查询结果中消失?
我编写了一些实验代码来了解锁定在 Hibernate 中的工作原理。它通过调整transaction()方法的参数来模拟两个事务,其关键步骤(选择和提交)可以以不同的顺序执行。这次用Field代替Entity,不过没关系。每个事务读取相同的Field,更新其description 属性并提交。
private static final LockMode lockMode = LockMode.PESSIMISTIC_WRITE;
enum Order {T1_READS_EARLIER_COMMITS_LATER, T2_READS_EARLIER_COMMITS_LATER};
@Test
public void firstReadsTheOtherRejected() {
ExecutorService es = Executors.newFixedThreadPool(3);
// It looks like the transaction that commits first is the only transaction that can make changes.
// The changes of the other one will be ignored.
final Order order = Order.T1_READS_EARLIER_COMMITS_LATER;
// final Order order = Order.T2_READS_EARLIER_COMMITS_LATER;
es.execute(() -> {
switch (order) {
case T1_READS_EARLIER_COMMITS_LATER:
transaction("T1", 1, 8);
break;
case T2_READS_EARLIER_COMMITS_LATER:
transaction("T1", 4, 1);
break;
}
});
es.execute(() -> {
switch (order) {
case T1_READS_EARLIER_COMMITS_LATER:
transaction("T2", 4, 1);
break;
case T2_READS_EARLIER_COMMITS_LATER:
transaction("T2", 1, 8);
break;
}
});
es.shutdown();
try {
es.awaitTermination(1, TimeUnit.MINUTES);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void transaction(String name, int delayBeforeRead, int delayBeforeCommit) {
Transaction tx = null;
Session session = null;
try {
session = factory.openSession();
tx = session.beginTransaction();
try {
TimeUnit.SECONDS.sleep(delayBeforeRead);
} catch (InterruptedException e) {
e.printStackTrace();
}
Query query = session.createQuery("SELECT f FROM Field f WHERE f.description=?1").setLockMode("this", lockMode);
query.setString("1", DESC);
Field field = (Field) query.uniqueResult();
String description1 = field.getDescription();
System.out.println(name + " : FIELD READ " + description1);
try {
TimeUnit.SECONDS.sleep(delayBeforeCommit);
} catch (InterruptedException e) {
e.printStackTrace();
}
field.setDescription(name);
session.update(field);
System.out.println(name + " : FIELD UPDATED");
tx.commit();
} catch (Exception e) {
fail();
if (tx != null) {
tx.rollback();
}
} finally {
session.close();
}
System.out.println(name + " : COMMITTED");
}
和输出:
T1 : FIELD READ This is a field for testing
апр 19, 2019 5:28:01 PM org.hibernate.loader.Loader determineFollowOnLockMode
WARN: HHH000445: Alias-specific lock modes requested, which is not currently supported with follow-on locking; all acquired locks will be [PESSIMISTIC_WRITE]
апр 19, 2019 5:28:01 PM org.hibernate.loader.Loader shouldUseFollowOnLocking
WARN: HHH000444: Encountered request for locking however dialect reports that database prefers locking be done in a separate select (follow-on locking); results will be locked after initial query executes
Hibernate: select field0_.ID as ID1_9_, field0_.DESCRIPTION as DESCRIPTION2_9_, field0_.NAME as NAME3_9_, field0_.TYPE as TYPE4_9_ from FIELD field0_ where field0_.DESCRIPTION=?
Hibernate: select ID from FIELD where ID =? for update
T1 : FIELD UPDATED
Hibernate: update FIELD set DESCRIPTION=?, NAME=?, TYPE=? where ID=?
T2 : FIELD READ This is a field for testing
T1 : COMMITTED
апр 19, 2019 5:28:07 PM org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl stop
T2 : FIELD UPDATED
Hibernate: update FIELD set DESCRIPTION=?, NAME=?, TYPE=? where ID=?
INFO: HHH000030: Cleaning up connection pool [jdbc:oracle:thin:@localhost:1521:oracle]
T2 : COMMITTED
Process finished with exit code 0
执行后,description 列包含T2。看起来pessimistic_write 模式有效。先写的交易 - 赢了。这是T2。但是 T1 发生了什么? T1 : COMMITTED 也出现在输出中。只要T1 没有改变任何我可以接受的东西,但我需要一个指示T1 失败,以便我可以重试读取/选择。
我错了。我多次运行代码并得到不同的结果。有时列描述包含 T1,有时包含 T2。
【问题讨论】:
-
好吧,Hibernate 中解决这个问题的典型方法是使用乐观锁定,让所有并发会话读取实体,并且只有第一个提交 更改,由于
version不匹配,所有其他人都会出现异常,必须重试。如果这是一个用户应用程序,请实现一些 pending 状态,这将限制其他用户的选择。 -
@MarmiteBomber '让所有并发会话读取实体,并且只有第一个提交更改'。似乎 PESSIMISTIC_WRITE 模式可以做到这一点。唯一的问题是获得其他人失败的指标。 T1失败了,我也不例外。请看代码。