JDBC 事务中如何检测死锁
jdbc 本身不直接提供“检测”死锁的 api,而是依赖底层数据库在提交或执行 sql 时抛出 sqlsyntaxerrorexception 或 sqlexception(错误码通常为 1213 或 40001)来通知应用层发生了死锁。开发者需要在代码中使用 try-catch 捕获这些异常,并通过检查 geterrorcode() 或 sqlstate 来判断是否为死锁,从而决定是重试还是回滚。

如何通过代码捕获并识别死锁异常
要准确识别死锁,必须捕获 java.sql.SQLException 并检查其错误码。不同数据库的死锁错误码不同:MySQL 通常是 1213,PostgreSQL 是 40P01,Oracle 是 60。以下是一个完整的 Java 示例,展示了如何在事务中处理死锁。

import java.sql.*;
public class DeadlockDetector {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "password";
try (Connection conn = DriverManager.getConnection(url, user, password)) {
conn.setAutoCommit(false);
// 模拟两个线程操作同一行数据导致死锁
Thread t1 = new Thread(() -> {
try {
updateBalance(conn, 1, 100);
conn.commit();
} catch (SQLException e) {
handleDeadlock(e);
}
});
Thread t2 = new Thread(() -> {
try {
updateBalance(conn, 2, 200);
conn.commit();
} catch (SQLException e) {
handleDeadlock(e);
}
});
t1.start();
t2.start();
t1.join();
t2.join();
} catch (Exception e) {
e.printStackTrace();
}
}
private static void updateBalance(Connection conn, int id, int amount) throws SQLException {
String sql = "UPDATE accounts SET balance = balance + ? WHERE id = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, amount);
pstmt.setInt(2, id);
pstmt.executeUpdate();
}
}
private static void handleDeadlock(SQLException e) throws SQLException {
int errorCode = e.getErrorCode();
String sqlState = e.getSQLState();
// MySQL 死锁错误码 1213, PostgreSQL 40P01
if (errorCode == 1213 || "40P01".equals(sqlState)) {
System.out.println("检测到死锁!正在回滚并重试...");
e.getConnection().rollback();
// 实际项目中应在此处加入重试逻辑
} else {
throw e;
}
}
}
运行上述代码时,如果两个线程同时尝试更新彼此持有的资源,其中一个线程会抛出 SQLException。通过 handleDeadlock 方法中的 getErrorCode() == 1213 判断,可以确认这是 MySQL 的死锁。此时调用 rollback() 回滚事务,避免脏数据。
死锁发生时的具体行为与重试策略
当死锁被数据库检测到后,数据库会选择牺牲一个事务(通常是代价较小的那个),将其回滚并抛出异常,而另一个事务则继续执行。在 JDBC 层面,这意味着 commit() 或 executeUpdate() 会立即失败。开发者需要实现重试机制,因为死锁往往是瞬时的。以下代码展示了如何封装一个带重试逻辑的事务执行器。

import java.sql.*;
public class RetryableTransaction {
private static final int MAX_RETRIES = 3;
private static final long RETRY_DELAY_MS = 100;
public static void executeWithRetry(Connection conn, Runnable task) throws SQLException {
int attempts = 0;
while (attempts = MAX_RETRIES) {
throw e;
}
try {
Thread.sleep(RETRY_DELAY_MS * attempts); // 指数退避
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new SQLException("重试中断", ie);
}
continue; // 重试
} else {
throw e; // 非死锁异常直接抛出
}
}
}
}
private static boolean isDeadlock(SQLException e) {
int errorCode = e.getErrorCode();
String sqlState = e.getSQLState();
return errorCode == 1213 || "40P01".equals(sqlState);
}
}
在上述实现中,executeWithRetry 方法接收一个 Runnable 任务。如果任务因死锁失败,它会等待一段时间后重试。这种模式确保了在高并发场景下,事务能够自动恢复,而不需要人工干预。注意,重试次数不宜过多,以免无限循环。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











