不同 OracleDB 连接之间的共享事务

Shared Transaction between different OracleDB Connections

经过几天时间调查这个问题后,我决定提交这个问题,因为发生的事情显然没有任何意义。

案例

我的计算机配置有本地 Oracle Express 数据库。 我有一个 JAVA 项目,其中有几个扩展父 class 的 JUnit 测试(我知道它不是 "best practice"),它打开一个 OJDBC 连接(使用静态10 个连接的 Hikari 连接池)在 @Before 方法中并在 @After 中回滚它。

public class BaseLocalRollbackableConnectorTest {
private static Logger logger = LoggerFactory.getLogger(BaseLocalRollbackableConnectorTest.class);
protected Connection connection;

@Before
public void setup() throws SQLException{
    logger.debug("Getting connection and setting autocommit to FALSE");
    connection = StaticConnectionPool.getPooledConnection();
}

@After
public void teardown() throws SQLException{ 
    logger.debug("Rollback connection");
    connection.rollback();
    logger.debug("Close connection");
    connection.close();
}

静态连接池

public class StaticConnectionPool {

private static HikariDataSource ds;

private static final Logger log = LoggerFactory.getLogger(StaticConnectionPool.class);

public static Connection getPooledConnection() throws SQLException {

    if (ds == null) {
        log.debug("Initializing ConnectionPool");
        HikariConfig config = new HikariConfig();
        config.setMaximumPoolSize(10);
        config.setDataSourceClassName("oracle.jdbc.pool.OracleDataSource");
        config.addDataSourceProperty("url", "jdbc:oracle:thin:@localhost:1521:XE");
        config.addDataSourceProperty("user", "MyUser");
        config.addDataSourceProperty("password", "MyPsw");
        config.setAutoCommit(false);
        ds = new HikariDataSource(config);

    }
    return ds.getConnection();

}

}

这个项目有数百个测试(非并行)使用此连接(在本地主机上)使用 Sql2o 执行查询(insert/update 和 select),但仅管理连接的事务和关闭外部(通过上面的测试)。 数据库为空,无法进行 ACID 测试。

所以预期的结果是向数据库中插入一些东西,进行断言然后回滚。这样第二次测试就不会发现之前测试添加的任何数据,以保持隔离级别。

问题 运行 所有测试一起(按顺序),90% 的时间它们都能正常工作。 10% 的一两个测试随机失败,因为之前的测试在数据库中有脏数据(例如重复的唯一数据)。查看日志,先前测试的回滚已正确完成。事实上,如果我检查数据库,它是空的) 如果我在具有更高性能但相同 JDK、相同 Oracle DB XE 的服务器上执行此测试,则此故障率会增加到 50%。

这很奇怪,我不知道,因为测试之间的连接不同,每次都会调用回滚。 JDBC 隔离级别是 READ COMMITTED 所以即使我们使用相同的连接,即使使用相同的连接也不应该产生任何问题。 所以我的问题是: 为什么会发生?你有什么主意吗?据我所知,JDBC 回滚是同步的,还是在某些情况下即使未完全完成也可以继续?

这些是我的主要数据库参数: 处理 100 会话 172 交易 189

如果您的问题只需要"solved"(例如不是"best practice")而不管性能如何只按顺序完成测试,请尝试设置:

config.setMaximumPoolSize(1);

您可能需要将超时设置得更高,因为测试队列中的测试将等待轮到它并可能超时。我通常不建议这样的解决方案,但您的设置不是最理想的,它会导致竞争条件和数据丢失。不过,祝你考试顺利。

尝试对 Oracle 中的所有语句配置审计。然后找到同时存在的会话。我认为测试中存在问题。 JDBC 回滚是同步的。 Commit 可以配置为 commit nowait,但我认为您在测试中并不特别。

另外关注并行dml。在同一事务中的一个 table 上,你不能在没有提交的情况下执行并行 dml + 任何其他 dml,因为你得到 Ora-12838。

你有自主交易吗?测试中的业务逻辑可以手动回滚它们,在测试期间,自主事务就像另一个会话,它看不到父会话的任何提交。

不确定这是否会解决问题,但您可以尝试:

public class BaseLocalRollbackableConnectorTest {
  private static Logger logger = LoggerFactory.getLogger(BaseLocalRollbackableConnectorTest.class);
  protected Connection connection;
  private Savepoint savepoint;

  @Before
  public void setup() throws SQLException{
    logger.debug("Getting connection and setting autocommit to FALSE");
    connection = StaticConnectionPool.getPooledConnection();
    savepoint = connection.setSavepoint();
  }

  @After
  public void teardown() throws SQLException{ 
    logger.debug("Rollback connection");
    connection.rollback(savepoint);
    logger.debug("Close connection");
    connection.close();
    while (!connection.isClosed()) {
      try { Thread.sleep(500); } catch (InterruptedException ie) {}
    }
}

确实有两个 'fixes' - 在关闭后循环以确保连接在返回池之前关闭。二、测试前创建保存点,测试后恢复。

正如所有其他答案所指出的,很难说所提供的信息出了什么问题。此外,即使您通过 audit 找到了当前问题,也不意味着您的测试没有数据错误。

但这里有一个替代方案:因为您已经有一个空白的数据库模式,您可以将它导出到一个 SQL 文件。然后每次测试前:

  1. 删除架构
  2. 再次重新创建架构
  3. 提供示例数据(如果需要)

这将节省大量调试时间,确保每次 运行 测试时数据库都处于原始状态。所有这些都可以在脚本中完成。

注意:Oracle Enterprise 具有 flashback function to support your kind of operation. Also, if you can manage to use Hibernate and the likes, there's other in-memory databases (like HSQLDB),您可以利用它来提高测试速度并保持数据集的一致性。

EDIT: It seems implausible, but just in case: connection.rollback() only takes effect if you don't call commit() before it.

我在 2-3 年前 运行 遇到过同样的问题(我花了很多时间才搞清楚这个问题)。问题是 @Before 和 @After 并不是 always 真正连续的。 [您可以通过在调试中启动进程并在注释方法中放置一些断点来尝试此操作。

编辑: 正如 Tonio 指出的那样,我不够清楚。 @Before 和@After 的顺序根据测试前和测试后的 运行ning 来保证。问题是在我的情况下,有时 @Before 和 @After 被搞砸了。

预计:

@Before -> test1() -> @After -> @Before -> @test2() -> @After

但有时我会遇到以下顺序:

@Before -> test1() -> @Before -> @After -> @test2() -> @After

我不确定这是不是错误。当时我深入研究了它,它似乎是某种(处理器?)调度相关的魔法。 在我们的案例中,该问题的解决方案是 运行 在单个线程上进行测试并手动调用 init 和清理进程......像这样:

public class BaseLocalRollbackableConnectorTest {
    private static Logger logger = LoggerFactory.getLogger(BaseLocalRollbackableConnectorTest.class);
    protected Connection connection;

    public void setup() throws SQLException{
        logger.debug("Getting connection and setting autocommit to FALSE");
        connection = StaticConnectionPool.getPooledConnection();
    }

    public void teardown() throws SQLException{ 
        logger.debug("Rollback connection");
        connection.rollback();
        logger.debug("Close connection");
        connection.close();
    }

    @Test
    public void test() throws Exception{
        try{
            setup();
            //test
        }catch(Exception e){ //making sure that the teardown will run even if the test is failing 
            teardown();
            throw e;
        }
        teardown();
    }
}

我还没有测试过,但一个更优雅的解决方案可能是同步同一对象上的@Before 和@After 方法。如果您有机会尝试一下,请更新我。 :)

希望它也能解决你的问题。

在从你的回答中确认我对单元测试中的回滚和事务行为并不生气之后,我深入检查了所有查询和所有可能的原因,幸运的是(是的......即使我为此感到羞耻,我让我的思想自由)一切都按预期工作(交易、之前、之后等)。

有一些查询获取一些复杂视图的结果(并彻底深入配置到 DAO 层)来识别单行信息。 此视图基于 MAX of a TIMESTAMP 以识别特定事件的最新事件(在现实生活中几个月后发生的事件)。

准备数据库以进行单元测试,这些事件按每个测试顺序添加。 在某些情况下,当同一事务下的这些插入查询特别快时,会在同一毫秒内添加更多与同一对象相关的事件(TIMESTAMP 是使用 JODA DateTime 手动添加的)和日期的 MAX,returns 两个或多个值。 出于这个原因,解释了这样一个事实,即在性能更高的 computers/servers 上,这种情况比速度较慢的更频繁。 此视图在更多测试中使用,并且根据测试,错误是不同的和随机的(作为主键添加的 NULL 值、重复的主键等)。

例如:在下面的INSERT SELECT查询中很明显这个错误:

INSERT INTO TABLE1 (ID,COL1,COL2,COL3) 
  SELECT :myId, T.VAL1, T.VAL2, T.VAL3 
  FROM MyView v 
  JOIN Table2 t on t.ID = v.ID
  WHERE ........

后面添加参数myId作为Sql2o参数

我的视图是

SELECT ID, MAX(MDATE) FROM TABLEV WHERE.... GROUP BY ...

当视图returns由于相同的最大日期至少有2个结果时,它失败了,因为ID是固定的(开始时由序列生成,但第二次使用参数存储)。这会生成违反的 PK 约束。

这只是一个案例,但由于这种随机行为让我(和我的同事)发疯了...

在这些事件插入之间添加 1 毫秒的睡眠,它是固定的。现在我们正在努力寻找不同的解决方案,即使这种情况(用户在同一毫秒内交互两次)不会在生产系统中发生 但重要的是没有魔法像往常一样发生!

现在你可以侮辱我了:)

你可以做一件事来增加人数。最大池大小的连接数,并在提交操作的同一位置回滚操作,而不是在@after 语句中使用它。 希望有用。