MySQL死锁重试需严格限定异常码1213/40001,包裹完整事务边界,配合幂等校验、指数退避与@Recover兜底,避免盲目重试放大问题。

死锁重试不是加个try-catch就完事
MySQL报Deadlock found when trying to get lock时,InnoDB已经回滚了其中一个事务,应用层必须主动重试——但盲目重试会放大问题。关键在于:重试必须包裹整个事务边界,不能只重试某条SQL;必须识别真实死锁错误码1213(或SQLState 40001),而非把超时、连接断开等其他异常也混进去重试;重试前要确保业务逻辑幂等,比如支付扣款必须带唯一业务单号,避免重复执行。
@Retryable注解在Spring Boot里怎么配才不翻车
Spring Retry的@Retryable用起来轻量,但默认配置极易踩坑:
-
include = {SQLException.class}太宽泛,必须显式限定exceptionExpression = "#exception.sqlState == '40001' or #exception.errorCode == 1213" -
maxAttempts = 3是安全底线,超过3次大概率说明不是偶发死锁,而是设计缺陷(比如锁顺序混乱或索引缺失) -
backoff = @Backoff(initialDelay = 100, multiplier = 1.5, maxDelay = 2000)——初始延迟不能设为0,否则重试风暴直接压垮DB;maxDelay建议不超过2秒,金融类业务甚至要压到500ms - 必须搭配
@Recover兜底方法,记录失败日志并触发告警,而不是静默吞掉第3次失败
手动重试逻辑里最容易漏掉的三件事
不用框架、手写重试时,这几个点几乎100%被忽略:
- 每次重试前必须清空JPA一级缓存(
entityManager.clear())或MyBatis的SqlSession,否则第二次执行可能读到第一次失败时残留的脏状态 - 不能在事务方法内部调用
Thread.sleep(),这会让事务长时间持锁;必须在事务外等待,重试时开启全新事务 - 重试次数和延迟值不能硬编码,应从配置中心动态加载,方便线上紧急调整(比如大促期间临时调高
maxAttempts)
为什么有些死锁重试后还是失败?先查这三处
如果重试2–3次仍持续失败,问题大概率不在重试机制本身,而在底层事务设计:
- 检查是否所有涉及多表更新的事务都统一了顺序——比如订单+库存操作,必须始终先
UPDATE order再UPDATE stock,哪怕入口不同也要通过SELECT ... ORDER BY id FOR UPDATE强制排序 - 确认WHERE条件是否走索引:用
EXPLAIN看执行计划,若出现type: ALL或key: NULL,说明锁范围过大,间隙锁极易引发连锁死锁 - 查看事务是否隐式拉长:比如在
@Transactional方法里调用了HTTP接口或文件IO,导致锁持有时间远超预期,这种必须拆出事务边界
SHOW ENGINE INNODB STATUS\G里那个被覆盖前的LATEST DETECTED DEADLOCK快照——它不会撒谎,但只留给你一次读取机会。


















