尽管隔离设置为“已提交”,但为啥并发数据库连接会看到彼此未提交的更改?

Posted

技术标签:

【中文标题】尽管隔离设置为“已提交”,但为啥并发数据库连接会看到彼此未提交的更改?【英文标题】:Why do the concurrent DB connections see each others uncommitted changes though isolation is set to "read committed"?尽管隔离设置为“已提交”,但为什么并发数据库连接会看到彼此未提交的更改? 【发布时间】:2015-01-25 15:44:24 【问题描述】:

我正在尝试进行一些测试,以了解如何使用事务隔离级别来解决各种并发问题。我从TRANSACTION_READ_COMMITED 开始,但最简单的情况并不符合我的预期。代码如下:

try(Connection connection1 = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) 
    connection1.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
    connection1.setAutoCommit(false);

    try(Connection connection2 = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) 
        connection2.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
        connection2.setAutoCommit(false);

        assertEquals(0, selectAll(connection1));
        assertEquals(0, selectAll(connection2));

        insertOne(connection1);
        assertEquals(0, selectAll(connection2)); // there is 1 row!
    

在这里,我设置了 2 个并发连接,在它们中启动事务,在第一个连接中进行更改,并希望在第二个连接中看不到它们。这不起作用:在连接 1 中所做的未提交更改对连接 2 可见。

我使用以嵌入式模式运行的 HSQLDB 2.3.2 和内存数据库。以下是我的 selectAll/insert 辅助方法的实现:

private static void initSchema() throws SQLException 
    try(Connection connection = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) 
        try (PreparedStatement s = connection.prepareStatement(
                "create table Notes(text varchar(256) not null)")) 
            s.executeUpdate();
        
    


private static int selectAll(Connection connection) throws SQLException 
    int count = 0;
    try (PreparedStatement s = connection.prepareStatement("select * from Notes")) 
        s.setQueryTimeout(1);
        try (ResultSet resultSet = s.executeQuery()) 
            while (resultSet.next()) 
                ++count;
            
        
    

    return count;


private static void insertOne(Connection connection) throws SQLException 
    try(PreparedStatement s = connection.prepareStatement("insert into Notes(text) values(?)")) 
        s.setString(1, "hello");
        s.setQueryTimeout(1);
        s.executeUpdate();
    

完整的测试可以在这里找到:https://gist.github.com/loki2302/aad49a5a2c26d5fda2b3

这段代码有问题,还是 HSQLDB 的行为不正常?

更新:重新阅读wiki后,我认为我这里的想法是错误的。我在这里看到的是“幻读”。 READ_COMMITTED 不保证幻读不会发生。我应该检查的是,用单行预先设置表格,通过connection1 更新它,并确保通过connection2 看不到此更改,除非更改已提交。此外,一般不保证此更改在提交后立即可见:它可能变得可见,但不能保证。

【问题讨论】:

如果你使用 mysql 数据库而不是 InnoDB 作为存储引擎会发生什么? 这不是幻读。当同一事务中的两次读取由于另一个事务的插入而返回不同的结果时,就会发生幻读。您发布的代码应该符合您的预期......也许您的数据库配置有问题。 幻读。我在插入之前和之后运行了 2 个相同的查询,它们提供了 不同的 结果。 Wiki 说:“在事务过程中,执行了两个相同的查询,并且第二个查询返回的行集合与第一个不同”。 @agori 然后请发布答案并准确解释为什么这是合法的以及如何防止它。这对我们很有帮助:) 我已经使用 MySQL InnoDB 表对其进行了测试,它的工作方式与您期望的示例一样有效。但实际的问题是它是否符合 ACID 的行为以及为什么。因此,要么 1) 是否已提交导致幻读的操作以成为合法的幻读或 2) 您的整个数据库配置为不以某种方式支持事务 【参考方案1】:

您的进程内数据库设置不适合您正在执行的测试。

替代方案是:

    尝试运行服务器并使用 MVCC 重复测试。 尝试为每个与 MVCC 和进程内数据库的连接使用单独的线程。

当在进程内使用时,如您的示例,HSQLDB 要求每个连接都由单独的线程拥有。否则 Java 并发特性将无法工作。

当使用单独的线程运行时,您需要使用 MVCC 事务模型才能使其工作。在您使用的默认锁定模式下,插入将锁定另一个连接直到提交。

【讨论】:

以上是关于尽管隔离设置为“已提交”,但为啥并发数据库连接会看到彼此未提交的更改?的主要内容,如果未能解决你的问题,请参考以下文章

精通Java事务编程-弱隔离级别之已提交读

精通Java事务编程-弱隔离级别之已提交读

PosegreSQL基础回顾(第 13 章 并发控制)

PosegreSQL基础回顾(第 13 章 并发控制)

MySQL事务并发问题和MVCC机制

MySQL隔离级别的实现