使用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);
}
}
}
答案 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();