【问题标题】:Hibernate concurrency issue with select max() and then insert max() + 1选择 max() 然后插入 max() + 1 的休眠并发问题
【发布时间】:2016-12-25 12:32:37
【问题描述】:

我有一个包含几列的表和一个为这些列的值递增的序列。对于(简化)示例:

A | B | SEQ
===========
x | x | 1
x | x | 2
x | x | 3
x | y | 1
x | x | 4
z | x | 1
x | y | 2

我有多个进程将使用带有 Hibernate 的 Spring 应用程序并接受 A 和 B,选择 A 和 B 的最大值,然后插入 max + 1

因此,对于上面的示例,如果有人提供 A = z 和 B = x,我们将选择、获取 1 并插入 z、x、2。

如果有人提供了 A = z 和 B = z(没有现有行匹配),该过程将查询并找到最大值(或最大值为零),然后插入 z、z、1。

假设一个简单的序列在这里不起作用,因为序列的列大小不会大到允许一个序列跨越所有 A 和 B 值,并且因为实际问题比上面提供的示例更复杂.

我可以想到四种解决这个问题的方法可能会起作用,但我想知道是否有更标准的方法可以做到这一点,我不确定如何实现这些方法中的任何一种使用 Hibernate 且不使用自定义 Oracle 功能:

  1. 执行select max(),尝试插入。如果存在并发问题,则其中一个线程将失败,并且将编写代码以根据需要重试多次,直到插入工作。使用 Hibernate,这可能意味着我们必须在事务中间手动刷新。
  2. 在执行初始select max() 时,执行select for update 或其他机制来锁定我们关心的行(匹配A 和B 的行)。这将阻止也试图找到最大值的其他进程,然后我们可以插入我们的行,当 txn 提交时,等待的选择将被允许选择。不过,如果表中当前没有匹配 A 和 B 的行,我不确定这将如何工作。
  3. 创建一个以原子方式执行最大选择和插入的存储过程。不过,如果可能的话,我想避免使用存储过程。
  4. 在 Hibernate 中,有没有办法进行插入 (select max() + 1) 并从插入到一个语句中的列中返回值?

【问题讨论】:

    标签: java spring oracle hibernate concurrency


    【解决方案1】:

    不确定这是否适合您。但我就是这样做的,因为尝试保持并发可能很复杂。

    首先我节省了插入的时间。

    INSERT INTO yourTable (`A`, `B`, `insertTime`) VALUES(x, x, NOW() );
    

    那么你的查询就变成了

    SELECT `A`, `B`,
           ROW_NUMER() OVER (PARTITION BY `A`, `B` ORDER BY `insertTime`) as SEQ
    FROM yourTable
    //optional to get the exact result you show
    ORDER BY `insertTime`
    

    编辑:如果您想删除并在最后一个序列之后继续增长,这可能不起作用。

    【讨论】:

    • 感谢您的建议。我们可能不会在此表中进行任何删除。 partition by 需要原生 SQL,我想避免这种情况。
    • 你太挑剔了,先不用storeproc,现在不用原生sql。甲骨文有row_number() over (Partition
    • 你检查过这个吗? stackoverflow.com/questions/3616321/…
    【解决方案2】:

    如果有聚合表呢:

    A | B | LAST_SEQ
    ===========
    x | x | 4
    z | x | 1
    x | y | 2
    

    然后您可以轻松地使用行级锁在第一个表中安全地生成记录。

    【讨论】:

    • 我们可以这样做,但我们不知道 A 和 B 的所有组合(实际场景中会涉及更多列),因此我们无法轻松地预先填充所有可能的A 和 B(或 A、B、C 等)的值。
    • 您不必预先制作它。您可以合并到该表中,然后简单地读取 LAST_SEQ 的结果值。但是这个JQL做不到,hibernate故意不支持MERGE
    • 如果没有要锁定的行,您就不能在行级别锁定某些东西。那么如果两个线程从表中选择,没有得到任何结果,然后尝试同时插入第一行会发生什么?
    • 如果 (A,B) 上有唯一索引,那么可以。
    • 此外,您不必选择&插入。您只需执行单个原子合并语句。
    【解决方案3】:

    您希望执行此 read-increment-update/insert 的事务可序列化(如“SET TRANSACTION ISOLATION LEVEL SERIALIZABLE”中),该隔离级别使数据库尝试查找事务将按顺序运行的顺序,并且得到相同的结果;如果不能,则不允许其中一些通过。我在 Postgres CLI 中尝试了您的方案,它不允许插入(提交时出错)或冲突更新(第二次更新完成时出错,事务中止);我认为 Oracle 的行为类似,但细节可能会有所不同。即使在不同的事务中选择的标准不同,这仍然有效:例如,第一个事务由 A 选择,第二个由 B 选择,两者都进行了更改,并且这些更改无论如何都会影响任何先前的选择——你会得到一个错误。然后,我猜,重试。注意可能与 SERIALIZABLE 隔离相关的死锁,这将导致错误(但您已经有重试,所以这很好)。

    顺便说一句,对于这种更新的正确性,REPEATABLE READ 隔离级别就足够了,但对于插入则不行。

    至于如何在 Hibernate 中设置不同的事务隔离级别(您可能不希望所有事务都可序列化,因为它会影响性能),我只能考虑从两个配置文件配置两个会话工厂(或手动在代码,或从一个文件配置并在其中一个文件的代码中更新隔离级别)。其中之一将具有“可序列化”隔离级别,您将专门通过它使用您的表。隔离级别的属性是“hibernate.connection.isolation”。

    在 Spring 中,使用 @Transactional(isolation=Isolation.SERIALIZABLE) 注释方法就足够了。

    【讨论】:

      【解决方案4】:

      这里有一个 SQL 解决方案,您可以使用 Hibernate 的原生 SQL 接口为简单起见。

      insert into myTable (A, B, LAST_SEQ) 
      select 'T', 'Y', 
      (
        case 
          when (select max(LAST_SEQ) from myTable where A = 'T' and B = 'Y') is null then 1
          else (select 1 + max(LAST_SEQ) from myTable where A = 'T' and B = 'Y')
        end
      );
      

      希望对你有帮助。

      【讨论】:

      • 谢谢。这可能有效,但我需要取回一些标识符,称之为 LAST_SEQ,它会告诉我如何读回我插入的行。
      猜你喜欢
      • 1970-01-01
      • 2018-06-03
      • 1970-01-01
      • 1970-01-01
      • 2011-07-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多