应只对sqlstate '40001'重试,因其是ansi标准、跨驱动稳定;错误消息字符串易失真,errno 1213在部分驱动中不可靠;重试须包裹整个事务并显式rollback与begin,配合指数退避(如10ms→30ms→90ms)、±20%随机抖动、上限3次,并重新加载关键数据。

只对 SQLSTATE '40001' 重试,其他错误一律不重试——这是唯一安全的起点。盲目重试主键冲突、连接超时或权限错误,会掩盖真实问题,甚至导致重复扣款、双写通知等数据错乱。
怎么准确捕获 MySQL 死锁错误
别信错误消息字符串。MySQL 报出的 Deadlock found when trying to get lock 在本地化环境、ORM 封装、日志截断后极易失真,不能靠 str(err).find("Deadlock") 判断。
- Python(PyMySQL/MySQLdb):优先检查
exc.sqlstate == '40001';次选exc.args[0] == 1213 - Java(JDBC):用
SQLException.getSQLState().equals("40001");getErrorCode()在部分驱动中不可靠 - Go(database/sql + mysql driver):必须
errors.As(err, &mysqlErr)提取底层错误,再判断mysqlErr.Number == 1213 - SQLAlchemy:先
errors.Unwrap()到原始 DBAPI 错误,再查orig.sqlstate或orig.args[0]
重试必须包裹整个事务块,不能只重试某条语句
事务中一条 UPDATE 触发 ERROR 1213,InnoDB 已强制回滚整个事务,连接状态已终结。此时若只重试那条语句,跳过 tx.Rollback() 和重新 db.Begin(),后续操作大概率 panic 报 sql: transaction has already been committed or rolled back。
- 每次重试前必须显式调用
tx.Rollback(),即使上一轮没Commit—— 否则连接池可能卡住 - 不能在事务内做“局部重试”:比如第一条语句失败后重试它,第二条还接着执行。这破坏原子性,余额可能被扣两次
- 重试函数体应包含完整流程:
db.Begin()→ 业务 SQL →tx.Commit()或tx.Rollback()
重试策略要带退避、限次、重载数据
裸 while True: sleep(0.1) 是最危险的做法:所有客户端在同一毫秒窗口重试,大概率再次撞上相同锁路径,引发二次死锁雪崩。
- 初始延迟从
10ms起步,按指数增长(如10ms → 30ms → 90ms),并加 ±20% 随机抖动 - 硬性限制重试次数 ≤3 次;第 3 次失败必须原样抛出错误,交由上层降级或告警
- 重试时业务对象状态已过期,必须重新加载关键数据:例如转账前要
SELECT balance FROM accounts WHERE id = ? FOR UPDATE,不能沿用旧对象里的balance字段 - 连接池最大连接数要预留余量,例如 20 并发 × 3 次重试 → 至少配 60 连接,否则容易耗尽
最容易被忽略的是加锁顺序一致性:重试能起作用的前提,是每次执行时对行加锁的顺序完全一致。如果 SELECT ... FOR UPDATE 不带 ORDER BY id,两次执行锁的行顺序可能不同,等于换一种方式死锁。











