可序列化的事务不能保护我免受双重插入

时间:2017-12-22 13:41:22

标签: oracle hibernate transactions spring-transactions

在我的场景中,流程引擎只允许同时运行流程执行的一个实例,拒绝其他实例。由于我的应用程序计划部署到集群,因此在节点中做出的决策反映全局系统状态非常重要。我知道有很多方法可以实现我的目标。

为此,我们正在实施利用标准ACID事务的数据库锁。我们有一个表PROCESS_RUNS,我们记录当前和历史执行。更好的说法:我们使用历史表作为共享锁机制,依靠数据库来处理并发。注意:ID不是通过DB序列生成的

| ID | PROCESS_ID | STATUS  | START_TIME | END_TIME |
|----|------------|---------|------------|----------|
| 1  | PROCESS1   | RUNNING | --         | --       |
| 2  | PROCESS1   | OK      | --         | --       |
| 3  | PROCESS2   | RUNNING | --         | --       |
| 4  | PROCESS3   | KO      | --         | --       |

此处仅限制是,在给定PROCESS_ID列的情况下,每个进程的只有一个实例可能处于RUNNING状态。应用程序处理所有逻辑(包括节点电源故障)以保持数据库一致。在上面的示例中,PROCESS1和PROCESS2正在运行,因此可以将PROCESS3或PROCESS4的新实例插入运行状态,但应用程序必须拒绝INSERT PROCESS1的新实例。

考虑以下Hibernate代码:

@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.SERIALIZABLE)
public boolean checkNotRunningAndInsert(String processId, ProcessRun run)
{
    DetachedCriteria criteria = forClass(getEntityClass()).add(eq("processId", requireNonNull(processId, "processId is required")))//
                                                          .add(eq("status", ProcessRunStatus.RUNNING))//
                                                          .setProjection(rowCount());

    long activeCount = DataAccessUtils.longResult(getHibernateTemplate().findByCriteria(criteria));

    if (activeCount == 0)
    {
        // OK, return
        getHibernateTemplate().save(requireNonNull(run, "run is required"));
        return true;
    }
    else if (activeCount == 1)
    {
        // OK, process running
        return false;
    }
    else // if (activeCount>1)
    {
        log.error("Looks like more than 1 instance of the process {} is running. This is a bug", processId);
        return false;
    }
}

以上代码执行以下操作:

SELECT COUNT(*) FROM PROCESS_RUNS WHERE PROCESS_ID = ? AND STATUS = 'RUNNING'

然后,如果没有行,它将发出

INSERT INTO PROCESS_RUNS (....,STATUS,....) VALUES (...,'RUNNING',...)

否则返回false。如果DBMS拒绝提交事务,Spring PessimisticLockingFailureException会发出@Transactional,并在我的业务代码中处理,只需重新发布该事务几次,直到它完全退出。调用代码是synchronized所以当我的应用程序在单个服务器上运行时,我100%保证没有两个线程可以同时访问该方法。

对于群集安装,同样不能说。我已经做了一个JUnit实验,证明在Oracle(也许只有Mysql,MS SQL和Postgres之间的Oracle)中,当两个线程(锁定在不同的对象上)同时执行同一个事务时,序列化约束有时不受尊重。

SERIALIZABLE隔离事件我希望如果两个事务同时运行,其中一个事件排队*或者其中一个被取消,尤其是在第一个事件执行SELECT之后。从教科书中,给定T1和T2必须保证以下不可能

T1: SELECT
T2: SELECT
T2: INSERT
T1: INSERT
T1: COMMIT
T2: COMMIT

如果我调试上面的代码并执行步骤,从而在几秒钟内手动保证上述语句的执行顺序,Oracle就会按预期运行,并且我使用显式SQLException回滚了两个事务中的一个。如果我重试第二个事务,我可以看到第一个事务已经提交,然后代码返回false,因为它应该运行

T1: SELECT
T2: SELECT
T2: INSERT
T1: INSERT
T1: COMMIT
T2: ROLLBACK
T3: SELECT
T3: COMMIT EMPTY

不幸的是,这就是我的问题,我已经看到,如果我让JUnit在野外运行 ,我会得到不可预知的结果。有时我的测试在两个线程之一中返回false后成功,通常没有调用返回false 并且我在RUNNING状态插入时得到两行相同的进程。结果是不可预测的,例如大约在50/50赔率上随机发生。我还可以看到包含两行进程的数据库,比JUnit断言只有一个带有false的线程返回

最后一个违反了我的申请限制。

以下是错误情况下显示SQL输出的日志

Hibernate: delete from PROCESS_RUNS
Hibernate: select count(*) as y0_ from PROCESS_RUNS this_ where this_.PROCESS_ID=? and this_.STATUS=?
Hibernate: select count(*) as y0_ from PROCESS_RUNS this_ where this_.PROCESS_ID=? and this_.STATUS=?
Hibernate: insert into PROCESS_RUNS (CONTEXT_CLASS, END_TIME, QUEUE_TIME, ESTIMATED_QUEUE_WAIT, EXCEPTION_CLASS, MODULE_ID, NODE_ID, ORGANIZATION_ID, PRIORITY, PROCESS_ID, REFERENCE_ID, START_TIME, STATUS, TRIGGER_ID, TRIGGER_TYPE, USER_ID, RUN_ID, END_CONTEXT, EXECUTION_EXCEPTION, START_CONTEXT, TRIGGER_CONTEXT) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into PROCESS_RUNS (CONTEXT_CLASS, END_TIME, QUEUE_TIME, ESTIMATED_QUEUE_WAIT, EXCEPTION_CLASS, MODULE_ID, NODE_ID, ORGANIZATION_ID, PRIORITY, PROCESS_ID, REFERENCE_ID, START_TIME, STATUS, TRIGGER_ID, TRIGGER_TYPE, USER_ID, RUN_ID, END_CONTEXT, EXECUTION_EXCEPTION, START_CONTEXT, TRIGGER_CONTEXT) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

正确的案例

Hibernate: delete from PROCESS_RUNS
Hibernate: select count(*) as y0_ from PROCESS_RUNS this_ where this_.PROCESS_ID=? and this_.STATUS=?
Hibernate: select count(*) as y0_ from PROCESS_RUNS this_ where this_.PROCESS_ID=? and this_.STATUS=?
Hibernate: insert into PROCESS_RUNS (CONTEXT_CLASS, END_TIME, QUEUE_TIME, ESTIMATED_QUEUE_WAIT, EXCEPTION_CLASS, MODULE_ID, NODE_ID, ORGANIZATION_ID, PRIORITY, PROCESS_ID, REFERENCE_ID, START_TIME, STATUS, TRIGGER_ID, TRIGGER_TYPE, USER_ID, RUN_ID, END_CONTEXT, EXECUTION_EXCEPTION, START_CONTEXT, TRIGGER_CONTEXT) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: insert into PROCESS_RUNS (CONTEXT_CLASS, END_TIME, QUEUE_TIME, ESTIMATED_QUEUE_WAIT, EXCEPTION_CLASS, MODULE_ID, NODE_ID, ORGANIZATION_ID, PRIORITY, PROCESS_ID, REFERENCE_ID, START_TIME, STATUS, TRIGGER_ID, TRIGGER_TYPE, USER_ID, RUN_ID, END_CONTEXT, EXECUTION_EXCEPTION, START_CONTEXT, TRIGGER_CONTEXT) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
14:47:25.441 [pool-2-thread-2] ERROR org.hibernate.engine.jdbc.spi.SqlExceptionHelper - ORA-08177: impossibile serializzare l'accesso per questa transazione

14:47:25.451 [pool-2-thread-2] ERROR org.hibernate.internal.SessionImpl - HHH000346: Error during managed flush [could not execute statement]
Hibernate: select count(*) as y0_ from PROCESS_RUNS this_ where this_.PROCESS_ID=? and this_.STATUS=?

我已与我的团队进行了交谈,我们讨论过行锁定,但在COUNT语句中实际上没有要锁定的行。

我试过几次修改我的代码:

  • 在SELECT之后添加一个100ms的睡眠似乎可以使错误的行为保持一致(即我总是在线程和DBeaver中的两行中没有错误。)
  • 在INSERT之后重新发出COUNT查询以检查RUNNING状态中只有一行没有添加任何内容
  • 删除睡眠可使错误保持一致
  • 将隔离级别更改为READ_UNCOMMITTED需要更改整个会话,可能会产生破坏性影响
  • 在我的情况下,将隔离更改为READ_COMMITTED不是我想要的

我不知道还有什么可以尝试。我需要通过Java代码或Hibernate配置来解决这个问题。我还要求与Oracle合作也适用于MS SQL,Mysql和Postgres

问题:当且仅当同一PROCESS_ID的其他行未处于状态RUNNING时,如何确保我的代码在表中插入新行?到Oracle 12c和事务隔离?

  • 一再指出,实际上没有数据库序列化事务,而是授予如果两者都成功,则操作结果与串行执行一致

编辑1:也许Oracle正在设计Oracle。如果从SQL的角度看事务,则只能看到两个SELECT,然后两个INSERT在不同的数据上运行。 SELECT的结果不会改变系统状态,SELECT的结果都不会用在VALUE的{​​{1}}中。所以串行执行的结果是两个插入。其他数据库正确实施的真正问题是,调用者代码使用INSERT的结果来决定是否执行SELECT,这就是Oracle DB永远不会出现的问题。能够提前知道。我记得我的DB2类(即INSERT,而不是IBM DB2),其中讨论了关于DBMS实现的类似问题

根据research(强调保留)

,上述情况可能属实
  

Oracle数据库允许可序列化事务修改行,只有在可序列化事务开始时已经提交了由其他事务做出的行的更改。当可序列化事务尝试更新或删除由可序列化事务开始后提交的其他事务更改的数据时,数据库会生成错误:

1 个答案:

答案 0 :(得分:1)

看起来这就是Oracle的设计方式。我的情景是一个WONTFIX/WORKSASEXPECTED案例。让我来描述一下原因。

虽然我们同意从高层的角度来看,实际结果等同于任何顺序执行,但只是Oracle没有意识到这一点。

在评论中,我们已经强调了Oracle保证updateddeleted数据的隔离。简单地说......

  1. 如果SELECT没有返回结果
  2. ,则无法锁定Oracle
  3. INSERT不是更新
  4. 我认为索引更新将属于事务保护案例,但目前已超出范围。

    我还推测,随着序列的更新,使用sequence会触发错误。但我没试过。最重要的是,Hibernate缓存序列值。

    结论

    Oracle仅在UPDATEDELETE期间实现对并发执行的保护。如果我对此不满意,我必须将数据库更改为例如相反,MS SQL Server提供了这种保护。