2

使用 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关于此问题的博客。

我想通过使用普通 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);
        }
    }
}
4

2 回答 2

5

我认为这样做:

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

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

于 2012-09-08T16:20:18.200 回答
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();
于 2012-09-06T21:24:17.413 回答