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 のデッドロックをプログラムで作成したいと考えています。再試行ロジックをテストできるように、テストはすぐにデッドロックを作成する必要があります。

ジェフの分析を読んでわかったことは、いくつかのデータだけを持っていて、それをたくさん読んで、少し書くだけでよいということです。

私は現在、テーブルを作成し、いくつかのテストデータをテーブルに書き込む短い Java プログラム (以下) を持っています。それらのプログラムは、数百のスレッドを起動します。各スレッドは、テスト データの更新または読み取りを行います。更新操作と読み取り操作の比率を変更しましたが、比率に関係なく、プログラムでデッドロックを作成することはできません。このバージョンのテスト プログラムには、私の再試行ロジックがありません。SQL Server のデッドロックが確実に発生するようになったら、それを追加します。

すべてのスレッドを 1 つのプロセスで実行すると、何らかの方法で 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;
    }
}

スレッド開始のランダム化は必要ありませんが、正確性には影響しません。スレッド スケジューラは、スレッドの実行を任意にインターリーブする必要があります。ただし、私の環境では、開始する 2 番目のスレッドがほぼ常にではありませんが、デッドロックの犠牲者であることがわかりました。

于 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 に答える