核心思路是快速识别、合理重试、降低冲突概率,而非避免所有死锁;需区分死锁与锁等待超时,统一资源访问顺序、缩小事务范围、优化索引与隔离级别,并结合监控诊断根因。

Java 中处理两个并发事务因死锁导致的 LockWaitTimeoutException(或 MySQL 的 Deadlock found when trying to get lock、PostgreSQL 的 40P01 等),核心思路不是“避免所有死锁”(这在复杂业务中几乎不可能),而是快速识别、合理重试、降低冲突概率。
明确异常类型和触发场景
不同数据库抛出的异常类名不同,但本质都是事务在等待锁时超时或被主动回滚:
- MySQL:常见
com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock或Lock wait timeout exceeded - PostgreSQL:
org.postgresql.util.PSQLException: ERROR: deadlock detected(SQLState = 40P01) - Oracle:
java.sql.SQLException: ORA-00060: deadlock detected while waiting for resource - Spring JDBC / JPA 默认可能包装为
CannotAcquireLockException或PessimisticLockingFailureException
注意:死锁(deadlock)≠ 锁等待超时(lock wait timeout)。前者是数据库检测到循环等待后主动回滚一个事务;后者是事务等锁太久被超时中断。两者都需重试,但死锁更应优先优化访问顺序。
用 @Transactional + 重试机制自动恢复
Spring 提供了声明式事务与重试能力,组合使用最简洁可靠:
立即学习“Java免费学习笔记(深入)”;
- 添加
@EnableRetry到配置类 - 在易冲突的服务方法上加
@Transactional和@Retryable - 指定只对死锁/超时类异常重试,设置最大次数和退避策略
示例:
Java 代码:
@Service
public class OrderService {
@Retryable(
value = { DeadlockLoserDataAccessException.class, // Spring 对死锁的封装
LockTimeoutException.class,
PessimisticLockingFailureException.class },
maxAttempts = 3,
backoff = @Backoff(delay = 100, multiplier = 2) // 第一次等 100ms,第二次 200ms...
)
@Transactional
public void createOrder(Long userId, Long productId) {
// 先查用户余额(SELECT ... FOR UPDATE)
User user = userMapper.selectForUpdate(userId);
// 再查商品库存(SELECT ... FOR UPDATE)
Product product = productMapper.selectForUpdate(productId);
// 扣减逻辑...
if (user.getBalance() >= product.getPrice()) {
user.setBalance(user.getBalance() - product.getPrice());
product.setStock(product.getStock() - 1);
userMapper.update(user);
productMapper.update(product);
}
}
<pre class="brush:php;toolbar:false;">@Recover
public void recover(Exception e, Long userId, Long productId) {
log.error("订单创建失败,已重试 3 次,userId={}, productId={}", userId, productId, e);
throw new BusinessException("下单失败,请稍后重试");
}}
从根源降低死锁概率
重试是兜底,治本要减少并发冲突机会:
-
统一资源访问顺序:多个事务操作多张表时,始终按相同顺序(如按表名字母、主键 ID 升序)加锁。例如:总是先锁
user再锁product,禁止一个事务先 user 后 product,另一个先 product 后 user -
缩小事务范围:把非数据库操作(如 HTTP 调用、计算、日志)移出
@Transactional方法。事务越短,持锁时间越短 - 避免在事务中用户交互或长耗时操作:比如不能在事务里等 MQ 回执、调第三方支付接口
-
合理选择隔离级别:读已提交(READ_COMMITTED)通常比可重复读(REPEATABLE_READ)锁更少;必要时才用
SELECT ... FOR UPDATE,优先考虑应用层校验+乐观锁 -
索引要到位:没走索引的
WHERE条件会导致全表扫描加锁,极大增加死锁风险
监控与诊断死锁现场
上线后需能快速定位谁在抢什么锁:
- MySQL:执行
SHOW ENGINE INNODB STATUS\G查看最近死锁详情,含事务 SQL、持有锁、等待锁信息 - 开启慢查询 + general log(谨慎)辅助分析高频更新路径
- Spring Boot Actuator + Micrometer 可暴露事务成功率、平均耗时等指标
- APM 工具(如 SkyWalking、Pinpoint)追踪跨服务事务链路,确认是否上游调用顺序不一致引发下游死锁
拿到死锁日志后,重点看「WAITING FOR THIS LOCK TO BE GRANTED」和「HOLDS THE LOCK(S)」两段,还原两个事务的加锁序列,再调整代码顺序。


















