【问题标题】:Java sql transactions. What am I doing wrong?Java sql 事务。我究竟做错了什么?
【发布时间】:2009-08-11 14:00:44
【问题描述】:

我编写了这个小测试,唯一的目的是为了更好地理解 jdbc 中的事务。尽管我按照文档进行了所有操作,但测试并不希望正常工作。

这是表结构:

CREATE TABLE `default_values` (
   `id` INT UNSIGNED NOT auto_increment,
   `is_default` BOOL DEFAULT false,
   PRIMARY KEY(`id`)
);

测试包含 3 个类:

public class DefaultDeleter implements Runnable
{

    public synchronized void deleteDefault() throws SQLException
    {
        Connection conn = null;
        Statement deleteStmt = null;
        Statement selectStmt = null;
        PreparedStatement updateStmt = null;
        ResultSet selectSet = null;

        try
        {
            conn = DriverManager.getConnection("jdbc:mysql://localhost/xtest", "root", "");
            conn.setAutoCommit(false);
            conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);

            // Deleting current default entry
            deleteStmt = conn.createStatement();
            deleteStmt.executeUpdate("DELETE FROM `default_values` WHERE `is_default` = true");

            // Selecting first non default entry
            selectStmt = conn.createStatement();
            selectSet = selectStmt.executeQuery("SELECT `id` FROM `default_values` ORDER BY `id` LIMIT 1");

            if (selectSet.next())
            {
                int id = selectSet.getInt("id");

                // Updating found entry to set it default
                updateStmt = conn.prepareStatement("UPDATE `default_values` SET `is_default` = true WHERE `id` = ?");
                updateStmt.setInt(1, id);
                if (updateStmt.executeUpdate() == 0)
                {
                    System.err.println("Failed to set new default value.");
                    System.exit(-1);
                }
            }
            else
            {
                System.err.println("Ooops! I've deleted them all.");
                System.exit(-1);
            }

            conn.commit();
            conn.setAutoCommit(true);
        }
        catch (SQLException e)
        {
            try { conn.rollback(); } catch (SQLException ex)
            {
                ex.printStackTrace();
            }

            throw e;
        }
        finally
        {
            try { selectSet.close(); } catch (Exception e) {}
            try { deleteStmt.close(); } catch (Exception e) {}
            try { selectStmt.close(); } catch (Exception e) {}
            try { updateStmt.close(); } catch (Exception e) {}
            try { conn.close(); } catch (Exception e) {}
        }
    }

    public void run()
    {
        while (true)
        {
            try
            {
                deleteDefault();
            }
            catch (SQLException e)
            {
                e.printStackTrace();
                System.exit(-1);
            }

            try
            {
                Thread.sleep(20);
            }
            catch (InterruptedException e) {}
        }
    }

}

public class DefaultReader implements Runnable
{

    public synchronized void readDefault() throws SQLException
    {
        Connection conn = null;
        Statement stmt = null;
        ResultSet rset = null;

        try
        {
            conn = DriverManager.getConnection("jdbc:mysql://localhost/xtest", "root", "");

            conn.setAutoCommit(false);
            conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);

            stmt = conn.createStatement();
            rset = stmt.executeQuery("SELECT * FROM `default_values` WHERE `is_default` = true");

            int count = 0;
            while (rset.next()) { count++; }

            if (count == 0)
            {
                System.err.println("Default entry not found. Fail.");
                System.exit(-1);
            }
            else if (count > 1)
            {
                System.err.println("Count is " + count + "! Wtf?!");
            }

            conn.commit();
            conn.setAutoCommit(true);
        }
        catch (SQLException e)
        {
            try { conn.rollback(); } catch (Exception ex)
            {
                ex.printStackTrace();
            }

            throw e;
        }
        finally
        {
            try { rset.close(); } catch (Exception e) {}
            try { stmt.close(); } catch (Exception e) {}
            try { conn.close(); } catch (Exception e) {}
        }
    }

    public void run()
    {
        while (true)
        {
            try
            {
                readDefault();
            }
            catch (SQLException e)
            {
                e.printStackTrace();
                System.exit(-1);
            }

            try
            {
                Thread.sleep(20);
            }
            catch (InterruptedException e) {}
        }
    }

}

public class Main
{

    public static void main(String[] args)
    {
        try
        {
            Driver driver = (Driver) Class.forName("com.mysql.jdbc.Driver")
                    .newInstance();
            DriverManager.registerDriver(driver);

            Connection conn = null;
            try
            {
                conn = DriverManager.getConnection("jdbc:mysql://localhost/xtest", "root", "");
                System.out.println("Is transaction isolation supported by driver? " +
                        (conn.getMetaData()
                        .supportsTransactionIsolationLevel(
                        Connection.TRANSACTION_SERIALIZABLE) ? "yes" : "no"));
            }
            finally
            {
                try { conn.close(); } catch (Exception e) {}
            }

            (new Thread(new DefaultReader())).start();
            (new Thread(new DefaultDeleter())).start();

            System.in.read();
            System.exit(0);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }

}

我编写了脚本,每次运行都会用 10 万条记录(其中一条是默认记录)填充表格。但是每次我运行这个测试时,输出都是:

驱动是否支持事务隔离?是的

未找到默认条目。失败。

这段代码有什么问题?

【问题讨论】:

  • 我看到一些答案开始谈论声明性事务和使用 Spring(我确实喜欢 Spring),但他们只会混淆情况。这个问题是可以解决的,并且您将更好地理解使用原始 JDBC,如果您开始将其包装在更高级别的框架(例如 JEE 或 Spring)中,这将变得模糊不清。从长远来看,我建议不要使用原始 JDBC,除非您处于真正受限的环境中。它很容易出错,而且是 PITA。
  • 你为什么认为 DefaultReader 应该成功?仅仅因为线程在删除器之前启动,并不意味着它会在删除发生之前实际运行?请记住,MyISAM 表不支持事务,InnoDB 表支持。 SERIALIZABLE 隔离级别可能会失败,如果有 2 个并发 SERIALIZABLE 事务正在进行,dbs 通常会失败事务。

标签: java jdbc transactions


【解决方案1】:

请确保您正在创建 InnoDB 表,MyISAM(默认)不支持事务。您可以将您的数据库创建更改为:

CREATE TABLE `default_values` (
   `id` INT UNSIGNED NOT auto_increment,
   `is_default` BOOL DEFAULT false,
   PRIMARY KEY(`id`)
) Engine=InnoDB;

另一个例子:MySQL transaction with accounting application

【讨论】:

  • 我很感激上面所有的 cmets 都在帮助你——但我建议你首先确保你使用的是一个符合事务的表——然后 Aaron Digulla 的观点出现了:哪个线程正在命中分贝第一。使用事务表后,您可以在编写器线程中休眠(一次写入 100 行类型的东西),然后确保删除启动编写器线程的中间事务。您应该会看到交易在做他们当时所做的事情。
  • 看来我真的犯了如此愚蠢的错误。将引擎更改为 InnoDB 应用程序后似乎运行良好。谢谢。
【解决方案2】:

我建议您添加一些断点并逐步检查每个数据库操作,以检查它们是否按照您的预期进行。您可以在数据库服务器上打开一个会话并设置事务隔离级别,以便读取未提交的数据。

还要检查在 MySql 中使用 'true' 对于布尔类型的数值 1 是否有效。

【讨论】:

  • 所有的操作本身都是正确的。使用 'true' 也很好(它不是用作文本(在括号中),而是用作内置常量)。我首先从 MySQL 控制台检查了所有查询。
【解决方案3】:

答案很简单:创建两个线程。它们彼此完全独立运行。由于您不以任何方式同步它们,因此无法判断哪个先进入数据库。如果读者是第一个,那么删除程序还没有开始,并且不会有带有is_default == true 的项目,因为删除程序还没有走那么远。

接下来,您已经完全隔离了两个事务 (Connection.TRANSACTION_SERIALIZABLE)。这意味着即使删除者有机会更新数据库,读者也只有在关闭连接并打开新连接后才能看到它。

如果不是这样,删除器比读取器慢,因此在读取器查找记录时使用is_default == true 更新记录的可能性很小。

[编辑] 现在你说测试开始时应该有一个带有is_default == true 的项目。请在启动两个线程之前添加一个测试以确保情况确实如此。否则,您可能正在寻找错误的错误。

【讨论】:

  • 关于线程 - 这是要求之一。测试作为可靠的应用程序工作,但最终的应用程序可能由几个完全独立工作的进程组成。关于事务 - 据我了解,完全隔离的事务应该提供封闭操作的原子性,因此内部的所有操作都作为一个可靠的操作处理(在我的例子中是 DELETE、SELECT、UPDATE)。我错了吗?
  • 我使用脚本在每次运行前清除和填充表格。我也手动检查了开始条件。
【解决方案4】:

如果您允许容器管理事务,您可以执行以下操作:

@Resource
private UserTransaction utx;

然后在你的代码中使用它:

utx.begin();

// atomic operation in here

utx.commit();

那么您就不必担心事务管理的复杂性了。

编辑:@Gris:是的,你是对的。我曾假设您正在开发一个网络应用程序。正如 pjp 所说,在这种情况下,spring 是一个不错的选择。或者 - 根据应用程序的大小和复杂性 - 您可以管理自己的事务。

【讨论】:

  • 但如果我理解正确的话,它只有在应用程序在 j2ee 容器中运行时才可用。但我的目标是独立的 j2se 应用程序,而不是 webapp。
  • 作为完全成熟的 j2ee 的替代方案,您可以使用实现 DataSource TransactionManager 的 Spring。
  • 2 pjp:你可能是对的。我会查看文档,但我不确定从其他方面是否可以接受使用这样的框架。
【解决方案5】:

有几点值得一提:

  1. 在测试之前填充数据库的脚本真的有效吗?尝试从 Java 代码内部对表格执行select count(*) ... 进行检查(这听起来可能很愚蠢,但我以前犯过这个错误)。

  2. 不要到处都使用System.exit(),因为这会使代码难以测试 - 即使您似乎没有 default==true 记录,看看删除器的作用可能会很有趣。

【讨论】:

    猜你喜欢
    • 2013-08-06
    • 1970-01-01
    • 2016-07-18
    • 2019-12-23
    • 2016-06-11
    • 2014-06-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多