【问题标题】:Prevent reading the same record with Hibernate防止使用 Hibernate 读取相同的记录
【发布时间】: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失败了,我也不例外。请看代码。

标签: java oracle hibernate jpa


【解决方案1】:

您说您想确保其他事务不会读取查询实体。

为此,您需要LockMode.PESSIMISTIC_WRITE。这不允许读取和更新。 LockMode.PESSIMISTIC_READ 不允许只更新。

带有 LockModeType.PESSIMISTIC_WRITE 的锁可以在一个 实体实例强制在尝试的事务之间进行序列化 更新实体数据。

查询时可以使用带有 LockModeType.PESSIMISTIC_WRITE 的锁 数据并且死锁或更新失败的可能性很高 在并发更新事务中。

【讨论】:

  • 看起来允许读取。请查看我添加到问题中的代码。
猜你喜欢
  • 1970-01-01
  • 2021-10-21
  • 2011-02-19
  • 2017-11-09
  • 1970-01-01
  • 1970-01-01
  • 2010-12-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多