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

时间:2012-09-06 20:53:51

标签: 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);
        }
    }
}

2 个答案:

答案 0 :(得分:4)

我认为这样做:

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;
    }
}

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

答案 1 :(得分:2)

这是创建死锁的伪代码。

thread A:
    conA.setAutoCommit(false); // use transactions
    UPDATE TABLE_A SET AVALUE=5
    sleep(5); // seconds
    UPDATE TABLE_B SET BVALUE=5
    conA.commit();

thread B:
    conB.setAutoCommit(false); // use transactions
    sleep(1); // let thread A go first
    UPDATE TABLE_B SET BVALUE=5
    UPDATE TABLE_A SET AVALUE=5
    conB.commit();