不能依赖jdbc自动重试死锁,需应用层实现幂等、事务隔离、指数退避的重试逻辑,并统一识别各数据库死锁异常。

在 JDBC 中处理数据库死锁时,**不能依赖 JDBC 自动重试**,因为死锁是数据库层面的并发控制机制触发的异常(如 MySQL 的 Deadlock found when trying to get lock,PostgreSQL 的 40P01 serialization_failure),JDBC 只负责抛出 SQLException。真正的重试逻辑必须由应用层主动实现,且需注意幂等性、事务边界和退避策略。
识别死锁异常
不同数据库抛出的 SQLState 或错误码不同,需针对性捕获:
-
MySQL:SQLState 通常为
40001或45000,更可靠的是检查异常消息是否包含"Deadlock found" -
PostgreSQL:SQLState =
40P01 -
Oracle:SQLState =
61000(ORA-00060)或错误码60 -
SQL Server:错误号
1205
建议封装统一判断方法,避免硬编码字符串匹配:
示例(简化):boolean isDeadlock(SQLException e) {
String sqlState = e.getSQLState();
int errorCode = e.getErrorCode();
return "40P01".equals(sqlState) || // PG
"40001".equals(sqlState) || // Standard serialization failure
errorCode == 1205 || // SQL Server
errorCode == 60 || // Oracle
e.getMessage().toLowerCase().contains("deadlock");
}
设计可重试的事务操作
重试必须包裹在**独立、可重复执行的业务单元**中,且该单元应具备幂等性(例如:基于唯一约束插入、乐观锁更新、状态机推进)。不要在已有事务中嵌套重试——Spring 的 @Transactional 默认不支持重试嵌套。
- 将核心写操作(如扣减库存、更新订单状态)抽成无状态方法,入参明确,不依赖外部 mutable 对象
- 每次重试都开启新事务(显式
Connection.setAutoCommit(false)或通过 Spring 的TransactionTemplate) - 避免在重试逻辑中持有非事务资源(如文件句柄、HTTP 连接),防止泄漏
实现带退避的重试循环
简单 while 循环 + 固定 sleep 容易打爆数据库;推荐指数退避(Exponential Backoff)+ 随机抖动(Jitter):
- 初始延迟 10ms,每次失败后翻倍(10 → 20 → 40 → 80…)
- 上限设为 500ms,避免长等待影响响应
- 每次延迟加入 ±20% 随机偏移,缓解重试洪峰
- 最大重试次数建议 3–5 次(超过大概率是设计问题,非临时冲突)
int maxRetries = 3; long baseDelayMs = 10; for (int i = 0; i <h3>结合 Spring 的最佳实践</h3><p>若使用 Spring,优先用 <code>@Retryable</code>(Spring Retry)配合 <code>@Transactional</code>,但注意两点:</p>
-
@Retryable必须标注在 public 方法上,且调用需走代理(不能 self-invocation) - 事务需由被重试方法自身开启(即方法内不假定已有事务),推荐用
TransactionTemplate显式控制 - 配置
include = {DeadlockLoserDataAccessException.class}(JdbcTemplate 抛此类型)或自定义异常分类器
对 JPA 用户,Hibernate 可能将死锁包装为 PessimisticLockException 或 OptimisticLockException,需按实际异常链向上查找根本原因。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











