死锁产生的原因
- 资源竞争:多个事务同时竞争有限的数据库资源,如锁、表、行等。例如,事务T1持有资源R1,想要获取资源R2;而事务T2持有资源R2,想要获取资源R1,此时就可能产生死锁。
- 事务顺序不当:如果多个事务以不同顺序访问相同资源,就容易出现死锁。比如,事务A按顺序锁定资源X和Y,事务B按顺序锁定资源Y和X,当并发执行时,就可能形成死锁。
- 锁的持有时间过长:事务长时间持有锁而不释放,增加了其他事务等待的时间,提高了死锁发生的概率。
检测死锁
- 数据库自动检测:许多数据库系统(如MySQL、Oracle等)自身具备死锁检测机制。数据库内部会定期检查事务等待图,如果发现存在环(表示死锁),就会选择一个牺牲者事务(通常是回滚代价最小的事务)进行回滚,以打破死锁。
- 应用层检测:可以通过监控数据库的锁等待状态来间接检测死锁。例如,通过JDBC获取数据库连接的元数据,查询当前数据库中锁的持有和等待情况。如果发现有事务长时间等待某个锁,且等待关系形成环,就有可能发生了死锁。
预防和解决死锁问题的措施
- 优化事务顺序:确保所有事务以相同顺序访问资源。比如,所有事务都先锁定资源A,再锁定资源B,这样就不会因为事务访问顺序不同而导致死锁。
- 减少锁的持有时间:在事务中尽早释放不需要的锁。例如,对只读数据可以使用共享锁,并在读取完成后立即释放锁,避免长时间持有锁影响其他事务。
- 设置合理的锁超时:通过JDBC设置事务的锁等待超时时间。如果一个事务等待锁的时间超过设定值,就自动回滚该事务,避免无限期等待导致死锁。例如,在获取连接后设置事务超时:
Connection conn = DriverManager.getConnection(url, username, password);
conn.setTransactionTimeout(5); // 设置超时时间为5秒
- 使用乐观锁:乐观锁假设在大多数情况下不会发生冲突,在更新数据时才检查数据是否被其他事务修改。通过版本号或时间戳机制实现,如:
// 假设数据库表中有version字段
// 先查询数据及版本号
String selectSql = "SELECT data, version FROM your_table WHERE id =?";
PreparedStatement selectStmt = conn.prepareStatement(selectSql);
selectStmt.setInt(1, id);
ResultSet rs = selectStmt.executeQuery();
if (rs.next()) {
String data = rs.getString("data");
int version = rs.getInt("version");
// 进行业务处理
// 更新数据并检查版本号
String updateSql = "UPDATE your_table SET data =?, version = version + 1 WHERE id =? AND version =?";
PreparedStatement updateStmt = conn.prepareStatement(updateSql);
updateStmt.setString(1, newData);
updateStmt.setInt(2, id);
updateStmt.setInt(3, version);
int rowsUpdated = updateStmt.executeUpdate();
if (rowsUpdated == 0) {
// 版本号不一致,说明数据已被其他事务修改,需要重新读取数据并处理
}
}
- 死锁后重试:当事务因死锁回滚时,应用程序可以捕获异常,并在适当的时间间隔后重试该事务,提高事务成功执行的概率。
try {
// 事务操作
conn.setAutoCommit(false);
// 执行SQL语句
conn.commit();
} catch (SQLException e) {
if (isDeadlockException(e)) { // 判断是否是死锁异常
// 等待一段时间后重试
Thread.sleep(1000);
// 重新执行事务操作
} else {
// 处理其他异常
}
}