死锁异常表现为“Deadlock found when trying to get lock”,是InnoDB检测到事务间循环等待而主动回滚一方所致;常见于高并发“先查后更”操作或更新顺序不一致场景,需应用层捕获该错误并配合指数退避重试,同时确保事务边界清晰、前置条件校验及最大重试次数限制。

死锁异常长什么样?Deadlock found when trying to get lock 就是它
MySQL 报这个错时,不是你 SQL 写错了,而是两个或多个事务互相卡住对方要的行锁(比如 A 锁了 row1 等 row2,B 锁了 row2 等 row1)。InnoDB 会主动干掉其中一个事务让它回滚,并抛出 Deadlock found when trying to get lock 错误。应用层看到的就是这个字符串——别试图解析堆栈,直接匹配错误信息最稳。
常见场景:高并发下单、库存扣减、积分更新这类“先查后更”且涉及多行/多表的操作;或者事务里执行顺序不一致(A 先更新 user 再更新 order,B 反过来)。
注意:LOCK WAIT TIMEOUT EXCEEDED 是另一个错误,属于锁等待超时,和死锁无关,不能混为一谈。
Java(Spring)里怎么加重试?用 @Transactional + @Retryable 不可靠
Spring Retry 的 @Retryable 默认不会重试已提交/回滚过的事务上下文。如果方法上同时有 @Transactional 和 @Retryable,第一次失败触发 rollback,第二次重试其实是在新事务里跑——但业务逻辑可能已经变了(比如库存被其他请求扣掉了),盲目重试反而引发超卖。
正确做法是把重试控制收归应用层,显式管理事务边界:
- 用
TransactionTemplate包裹核心逻辑,每次重试都开新事务 - 重试前检查关键前置条件(如当前库存是否仍充足),避免无效重试
- 限制最大重试次数(通常 2–3 次足够,再多大概率是设计问题)
- 捕获异常时只针对
DeadlockLoserDataAccessException(Spring)或原生SQLException中的 SQLState40001或错误信息含Deadlock found when trying to get lock
示例片段(Spring Boot):
int retry = 0;
while (retry {
// 扣库存、写订单等操作
stockMapper.decrease(productId, 1);
orderMapper.insert(new Order(...));
return null;
});
break; // 成功就跳出
} catch (DataAccessException e) {
if (isDeadlock(e) && retry <h3>Python(SQLAlchemy)重试的关键:手动控制 <code>session.rollback()</code> 和 <code>session.close()</code>
</h3><p>SQLAlchemy 的 <code>session</code> 是有状态的。一旦发生死锁,session 进入不可用状态,不显式 <code>rollback()</code> 就直接重试,下次 <code>commit()</code> 会报 <code>InvalidRequestError: This session is in 'prepared' state</code>。</p><p>必须确保每次重试都从干净的 session 开始:</p>
- 捕获
exc.DBAPIError后,立刻调用session.rollback() - 紧接着调用
session.close()(或用新 session 替换) - 不要在同一个 session 上反复
rollback()+commit()—— 它不是可重入的 - 重试间隔建议用指数退避,比如第 1 次等 10ms,第 2 次等 30ms,避免雪崩式重试冲击数据库
判断死锁的简易方式:
def is_deadlock_error(e):
return (
isinstance(e, exc.DBAPIError) and
e.orig and
getattr(e.orig, 'args', None) and
any('Deadlock found' in str(arg) for arg in e.orig.args)
)
重试不是万能解药:哪些情况加了也白加
死锁重试只能缓解“偶然竞争”,掩盖不了结构性问题:
- 批量更新时按主键升序排列 where 条件(如
WHERE id IN (100, 50, 200)改成WHERE id IN (50, 100, 200)),否则不同事务更新顺序天然不一致 - 避免在事务里做 RPC、HTTP 调用或耗时计算——锁持有时间越长,死锁概率越高
- 单条
UPDATE语句尽量走索引,全表扫描会锁整张表,放大冲突面 - 如果 5% 以上的请求都触发死锁重试,说明表结构、索引或业务流程该重新看了
真正难处理的,是那些跨服务、跨数据库、甚至带最终一致性要求的场景——这时候重试逻辑得和 Saga 或本地消息表联动,单靠 MySQL 层的死锁捕获已经不够用了。











