25

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

案子

我的计算机配置了本地 Oracle Express 数据库。我有一个 JAVA 项目,其中包含几个扩展父类的 JUnit 测试(我知道这不是“最佳实践”),它在 @Before 方法中打开一个 OJDBC 连接(使用 10 个连接的静态 Hikari 连接池)并滚动在@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 执行查询(插入/更新和选择),但连接的事务和关闭只在外部管理(通过上面的测试)。数据库完全为空,无法进行 ACID 测试。

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

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

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

这些是我的主要数据库参数:处理 100 个会话 172 个事务 189

4

7 回答 7

4

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

编辑:正如托尼奥指出的那样,我不够清楚。@Before 和@After 的顺序保证了测试之前和之后的运行。就我而言,问题是有时 @Before 和 @After 搞砸了。

预期的:

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

但有时我遇到以下顺序:

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

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

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 方法。如果您有机会尝试一下,请更新我。:)

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

于 2016-07-12T12:08:44.523 回答
1

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

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) {}
    }
}

确实有两个“修复” - 关闭后循环以确保连接在返回池之前关闭。其次,在测试之前创建一个保存点并在之后恢复它。

于 2016-07-08T18:29:58.213 回答
1

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

还要注意并行dml。在同一事务中的一张表上,您无法在没有提交的情况下执行并行 dml + 任何其他 dml,因为您获得了 Ora-12838。

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

于 2016-07-08T07:08:59.090 回答
1

如果您的问题只需要“解决”(例如,不是“最佳实践”)而不管性能如何,只需按顺序完成测试,请尝试设置:

config.setMaximumPoolSize(1);

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

于 2016-07-07T21:28:12.070 回答
1

在您的回答确认我对单元测试中的回滚和事务行为并不生气之后,我深入检查了所有查询和所有可能的原因,幸运的是(是的,不幸的是......即使我为此感到羞耻,我也会下定决心免费)所有工作都按预期进行(交易、之前、之后等)。

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

准备数据库以进行单元测试,每个测试按顺序添加这些事件。在某些情况下,当同一事务下的这些插入查询特别快时,与同一对象相关的更多事件会在同一毫秒内添加(TIMESTAMP 是使用 JODA DateTime 手动添加的)和日期的 MAX,返回两个或更多的价值。出于这个原因,解释了这样一个事实,即在性能更高的计算机/服务器上,这种情况比速度较慢的计算机/服务器更频繁地发生。此视图用于更多测试,并且根据测试,错误是不同的且随机的(作为主键添加的 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 ...

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

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

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

现在你可以侮辱我了:)

于 2016-07-13T16:56:10.887 回答
1

就像所有其他答案所指出的那样,很难说所提供的信息出了什么问题。此外,即使您设法通过审计找到当前问题,也并不意味着您的测试没有数据错误。

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

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

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

注意:Oracle Enterprise 具有闪回功能以支持您的操作。此外,如果您可以设法使用Hibernate等,还有其他内存数据库(如HSQLDB)可以用来提高测试速度并保持数据集的一致性。

编辑:这似乎不可信,但以防万一:connection.rollback()只有在你之前不调用commit() 时才会生效。

于 2016-07-11T02:59:11.010 回答
-1

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

于 2016-07-14T07:14:41.173 回答