关于jdbc:Java sql事务。我究竟做错了什么?

 2021-04-09 

Java sql transactions. What am I doing wrong?

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

这是表结构:

1
2
3
4
5
CREATE TABLE `default_values` (
   `id` INT UNSIGNED NOT auto_increment,
   `is_default` BOOL DEFAULT false,
   PRIMARY KEY(`id`)
);

测试包含3个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
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();
        }
    }

}

我已经编写了脚本,每次运行都会为该表填充100k条记录(其中一个是默认记录)。但是每次我运行此测试时,输出为:

Is transaction isolation supported by driver? yes

Default entry not found. Fail.

此代码有什么问题?


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

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

另一个示例:带有记帐应用程序的MySQL事务


有两点值得一提:

  • 在测试真正起作用之前,您的脚本是否会填充数据库?尝试从Java代码内部在表上执行select count(*) ...进行检查(这听起来可能很愚蠢,但是我之前犯过此错误)。

  • 请勿在所有地方执行System.exit(),因为这会使代码难以测试-即使似乎没有默认== true记录,也可以看到删除程序的作用,这很有趣。


  • 答案很简单:您创建了两个线程。它们彼此完全独立运行。由于您不以任何方式进行同步,因此无法确定哪个人先进入数据库。如果阅读器是第一个阅读器,则删除器尚未开始,并且不会有is_default == true项,因为删除器还没有到此。

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

    如果不是这种情况,则删除器的速度比读取器慢,因此在读取器寻找记录时用is_default == true更新记录的机会很小。

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


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

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


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

    1
    2
    @Resource
    private UserTransaction utx;

    ,然后在您的代码中使用它:

    1
    2
    3
    4
    5
    utx.begin();

    // atomic operation in here

    utx.commit();

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

    编辑:@Gris:是的,您是正确的。我以为您正在开发Web应用程序。正如pjp所说,在这种情况下,spring是一个不错的选择。或者-根据应用程序的大小和复杂性-您可以通过管理自己的事务来实现。