java - 立即创建 SQL Server 死锁的最小 Java JDBC 程序?

标签 java sql-server jdbc deadlock

使用SQL Server时有时会出现这样的错误:

Transaction (Process ID 54) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.

有关更多背景信息,请查看 Jeff Atwood 的 blogged关于这个问题。

我想通过使用普通旧式 JDBC 的小测试以编程方式创建 SQL Server 死锁。测试应该立即创建一个死锁,以便我可以测试一些重试逻辑。

根据阅读 Jeff 的分析,我的理解是我只需要有一些数据并大量阅读,然后写一点。

我目前有一个简短的 Java 程序(如下),它创建一个表并将一些测试数据写入该表。他们的程序启动了数百个线程。每个线程要么执行更新,要么读取测试数据。我改变了更新与读取操作的比率,但无论比率如何,我似乎都无法以编程方式创建死锁。这个版本的测试程序没有我的重试逻辑,一旦我可以可靠地让 SQL Server 死锁发生,我将添加它。

我想知道让所有线程在一个进程中运行是否会以某种方式在 JDBC 驱动程序级别序列化操作,所以我尝试并发运行多个进程(在同一台机器上),但仍然没有死锁。

import java.sql.*;
import java.util.*;
import java.util.concurrent.*;
import static java.util.concurrent.TimeUnit.*;

public class Deadlock {
    static final int QUERY_THREAD_COUNT = 300, MAX_OPERATIONS_ITERATION = 5000;
    static String server, db, user, pw;
    static CountDownLatch latch = new CountDownLatch(QUERY_THREAD_COUNT);

    public static void main(String... args) throws Exception {
        server = args[0];
        db     = args[1];
        user   = args[2];
        pw     = args[3];
        Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
        Connection connection = getConnection();
        Statement statement = connection.createStatement();
        statement.execute("CREATE TABLE TESTTABLE (BAR INTEGER, BAZ VARCHAR(32))");
        statement.execute("DELETE FROM TESTTABLE");
        statement.execute("INSERT INTO TESTTABLE VALUES (1, 'FOOBARBAZ')");
        connection.setAutoCommit(false);
        connection.commit();
        connection.close();
        ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
        for (int i = 0; i < QUERY_THREAD_COUNT; ++i) {
            scheduledExecutorService.scheduleWithFixedDelay(new Operation(), 0, 1, MILLISECONDS);
        }
        latch.await();
        System.exit(0);
    }

    static class Operation implements Runnable {
        Connection connection = getConnection();
        Statement statement = getStatement(connection);
        int iteration;

        @Override
        public void run() {
            if (++iteration > MAX_OPERATIONS_ITERATION) {
                latch.countDown();
                return;
            }
            try {
                double  random = Math.random();
                boolean update = (random < 0.01);
                if (update) {
                    statement.executeUpdate("UPDATE TESTTABLE SET BAR=" + ((int) (random * 100)) + " WHERE BAZ='FOOBARBAZ'");
                } else {
                    ResultSet rs = statement.executeQuery("SELECT BAR, BAZ FROM TESTTABLE");
                    if (! rs.next()) {
                        return;
                    }
                    int    bar = rs.getInt(1);
                    String baz = rs.getString(2);
                    if (bar > 100) {
                        System.err.println("int is greater than 100");
                    }
                    if (! baz.equals("FOOBARBAZ")) {
                        System.err.println("string is not FOOBARBAZ");
                    }
                }
                connection.commit();
            } catch (SQLException sqle) { // <-- looking for a deadlock exception here!
                System.err.println(sqle);
            }
        }
    }

    static Connection getConnection() {
        try {
            return DriverManager.getConnection("jdbc:sqlserver://" + server + ";databaseName=" + db + ";", user, pw);
        } catch (Exception e) {
            System.err.println(e);
            throw new RuntimeException(e);
        }
    }

    static Statement getStatement(Connection connection) {
        try {
            return connection.createStatement();
        } catch (Exception e) {
            System.err.println(e);
            throw new RuntimeException(e);
        }
    }
}

最佳答案

我认为这样做:

import java.sql.*;
import java.util.*;
import java.util.concurrent.*;

/**
 * Creates an SQL Server deadlock.
 *
 * <pre>
   javac SQLServerDeadlock.java && java -cp ".:sqljdbc.jar" SQLServerDeadlock <server> <db-name> <username> <password>
 * </pre>
 */
public class SQLServerDeadlock {
    static String server, db, user, pw;
    static String TABLE_A = "TABLE_A", TABLE_B = "TABLE_B";
    static CountDownLatch latch = new CountDownLatch(2);

    public static void main(String... args) throws SQLException {
        server = args[0];
        db     = args[1];
        user   = args[2];
        pw     = args[3];
        Connection connection = null;
        try {
            Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
            connection = getConnection();
            init(connection);
            Thread t1 = new Thread(new Update(TABLE_A, TABLE_B), "A-THEN-B");
            Thread t2 = new Thread(new Update(TABLE_B, TABLE_A), "B-THEN-A");
            if (Math.random() < .5) {
                t1.start();
                t2.start();
            } else {
                t2.start();
                t1.start();
            }
            t1.join();
            t2.join();
        } catch (Exception e) {
            System.err.println(e);
        } finally {
            cleanup(connection);
        }
    }

    static class Update implements Runnable {
        String table1;
        String table2;

        Update(String table1, String table2) {
            this.table1 = table1;
            this.table2 = table2;
        }

        @Override
        public void run() {
            Connection connection = null;
            try {
                connection = getConnection();
                Statement statement = connection.createStatement();
                statement.executeUpdate("UPDATE " + table1 + " SET FOO=1");
                latch.countDown();
                latch.await();
                statement.executeUpdate("UPDATE " + table2 + " SET FOO=1");
                connection.commit();
                System.err.println(Thread.currentThread().getName() + ": SUCCESS!");
            } catch (SQLException sqle) {
                if (sqle.getMessage().contains("Rerun the transaction")) {
                    System.err.println(Thread.currentThread().getName() + ": DEADLOCK VICTIM!");
                }
                System.err.println(sqle);
            } catch (InterruptedException ie) {
                System.err.println(ie);
            } finally {
                try {
                    connection.close();
                } catch (SQLException sqle) {
                    System.err.println(sqle);
                }
            }
        }
    }

    static void init(Connection connection) throws SQLException {
        Statement statement = null;
        try {
            statement = connection.createStatement();
            for (String tableName : Arrays.asList(TABLE_A, TABLE_B)) {
                if (tableExists(connection, tableName)) {
                    statement.execute("DROP TABLE " + tableName);
                }
                statement.execute("CREATE TABLE " + tableName + " (FOO INTEGER)");
                statement.execute("INSERT INTO  " + tableName + " VALUES (0)");
            }
            connection.commit();
        } finally {
            statement.close();
        }
    }

    static void cleanup(Connection connection) throws SQLException {
        if (connection == null) {
            return;
        }
        Statement statement = null;
        try {
            statement = connection.createStatement();
            for (String tableName : Arrays.asList(TABLE_A, TABLE_B)) {
                if (tableExists(connection, tableName)) {
                    statement.execute("DROP TABLE " + tableName);
                }
            }
            connection.commit();
        } finally {
            statement.close();
        }
    }

    static boolean tableExists(Connection connection, String tableName) throws SQLException {
        Statement statement = null;
        try {
            statement = connection.createStatement();
            String sql =
                " SELECT TABLE_NAME                         " +
                "   FROM INFORMATION_SCHEMA.TABLES          " +
                "  WHERE TABLE_CATALOG = '" + db        + "'" +
                "    AND TABLE_NAME    = '" + tableName + "'";
            ResultSet rs = statement.executeQuery(sql);
            return rs.next();
        } finally {
            statement.close();
        }
    }

    static Connection getConnection() throws SQLException {
        Connection connection = DriverManager.getConnection("jdbc:sqlserver://" + server + ";databaseName=" + db + ";", user, pw);
        connection.setAutoCommit(false);
        return connection;
    }
}

线程启动的随机化不是必需的,但不会影响正确性。线程调度程序应该任意交错执行线程。但是,在我的环境中,我观察到第二个启动的线程几乎(但并非总是)成为死锁受害者。

关于java - 立即创建 SQL Server 死锁的最小 Java JDBC 程序?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/12308234/

相关文章:

java - ADF 可视化 : Retrieve current managed bean

java - 如何通过 OSGI 包中的 kerberos 与 hdfs 连接

java - java中的链接 HashMap

java - 检索 ResultSet 值时获取 IN 参数而不是行

sql - 创建用户动态名称并分配角色

java - 为什么日期插入为 02/10/0010 而不是 02/10/2010

hibernate - Sybase DBMS 使用哪个连接 JAR?

java - 在 Java 中生成任意长度的任意随机数

sql - 复杂的 SQL WHERE 子句问题

java - Hibernate、JPA、JDBC 还是?