应只对 error 1213 或 sqlstate '40001' 重试,优先用 sqlstate == '40001' 判断;重试必须包裹整个事务块,结合指数退避、限次(≤3)、重载数据及索引优化根治。

只对 ERROR 1213 或 SQLSTATE '40001' 重试,其他错误一律不重试——这是最核心的判断前提。盲目重试非死锁错误(比如主键冲突 1062、连接中断、权限拒绝)不仅无效,还会掩盖真实问题,甚至引发数据错乱。
怎么准确捕获 MySQL 死锁错误?别信错误消息字符串
MySQL 报出的死锁错误长这样:Deadlock found when trying to get lock; try restarting transaction,但**不能靠匹配这段文字来判断**——本地化语言、日志截断、ORM 包装都会让它失效。
- Python(PyMySQL/MySQLdb):检查
exc.args[0] == 1213,或更稳妥地用exc.sqlstate == '40001' - Java(JDBC):优先用
SQLException.getSQLState().equals("40001"),getErrorCode()在部分驱动中不可靠 - Go(database/sql + mysql driver):需用
errors.As(err, &mysqlErr)提取底层错误,再判断mysqlErr.Number == 1213 - SQLAlchemy:必须
errors.Unwrap()到原始 DBAPI 错误,再查orig.args[0]或orig.sqlstate
重试必须包裹整个事务,不能只重试某条语句
事务中一条 UPDATE 触发 ERROR 1213,说明整个事务已被 InnoDB 强制回滚,状态已终结。此时若只重试那条语句,而跳过 tx.Rollback() 和重新 db.Begin(),后续操作会直接 panic,报 sql: transaction has already been committed or rolled back。
- 重试块起点是
db.Begin(),终点是tx.Commit()成功或明确判定非死锁错误 - 每次重试前必须调
tx.Rollback()(即使上一轮没显式 Commit),否则连接池可能卡住 - 不能在事务内做“局部重试”:比如第一条 UPDATE 失败后重试它,第二条还接着执行——这破坏原子性,余额扣了两次、通知发了两遍都可能发生
重试策略要带退避、限次数、重载数据
裸 while true + sleep(0.1) 是最常见也最危险的做法:所有客户端在同一毫秒窗口重试,大概率再次撞上相同锁路径,形成二次死锁雪崩。
- 初始延迟从
10ms起步,按指数增长(如10ms → 30ms → 90ms),并加随机抖动(±20%) - 硬性限制重试次数,建议 ≤3 次;第 3 次失败必须原样抛出错误,交由上层降级或告警
- 重试时业务对象状态已过期,必须重新加载关键数据:例如转账前要
SELECT balance FROM accounts WHERE id = ? FOR UPDATE,不能沿用旧对象里的balance字段 - 连接池最大连接数要预留余量,例如 20 并发 × 3 次重试 → 至少配 60 连接,否则容易触发
Too many connections
最容易被忽略的一点:死锁重试只是兜底手段,不是根治方案。如果同一段逻辑频繁触发 ERROR 1213,说明加锁顺序不一致、索引缺失或事务粒度太大——这时候该看 SHOW ENGINE INNODB STATUS\G 和开启 innodb_print_all_deadlocks = ON,而不是堆更多重试参数。











