应只对ERROR 1213或SQLSTATE '40001'重试,其他错误一律不重试;重试必须包裹整个事务块,结合指数退避、限次(≤3)、重载数据及索引优化根治。

只捕获 1213 错误码,别碰其他异常
MySQL死锁报错固定为 ERROR 1213 (40001),错误信息里一定含 Deadlock found when trying to get lock。其他错误比如 1062(主键冲突)、1205(锁等待超时)、2006(连接断开)都不能重试——它们不是资源争抢问题,重试只会重复失败甚至放大副作用。
常见踩坑点:
- 用字符串匹配
"Deadlock"而不校验错误码,结果把日志里偶然出现的单词当真 - 在 ORM 的
save()或update()内部加通用重试,导致1062也被反复执行,写入大量重复数据 - 没区分 SQLSTATE 和 errno:Python 的
mysql.connector用e.errno == 1213,Go 的database/sql需用errors.As(e, &mysql.MySQLError{Number: 1213})
重试必须配合重新加载数据,不能“原样再跑一遍”
事务被 MySQL 回滚后,应用内存里的对象状态和数据库实际状态已经脱节。比如你读取了用户余额是 100 元,扣 20 元后发生死锁;重试时若直接用旧值 100 - 20 = 80 更新,就忽略了中间可能已被别人改成 90 的事实。
实操建议:
- 所有写操作前,用
SELECT ... FOR UPDATE重新查最新值(注意:必须在同一事务内,且语句顺序不能颠倒) - 涉及幂等性场景(如库存扣减),强制使用 CAS 字段,例如
UPDATE stock SET qty = qty - 1 WHERE id = 123 AND qty >= 1 AND version = 5 - 避免在事务里调外部服务(HTTP、发 MQ),否则重试会导致消息重复、支付重复扣款等不可逆副作用
用带随机扰动的指数退避,别设固定间隔
第 1 次失败后等 10ms,第 2 次等 20ms,第 3 次等 40ms……上限控制在 500ms 内。但关键是要加 ±10% 随机抖动,否则所有客户端在第 2 次重试时集体醒来,反而加剧冲突。
示例(Python):
delay = min(0.01 * (2 ** i), 0.5) time.sleep(delay * (0.9 + random.random() * 0.2))
容易忽略的细节:
- 退避基数选 10ms 是因 InnoDB 行锁释放通常在毫秒级;设成 1ms 太短,没意义;设成 100ms 又拖慢平均响应
- 最大重试次数建议 ≤3 次,再不成功说明业务逻辑本身存在结构性争抢(比如总按不同顺序更新
users和orders表) - 每次重试必须复用同一个
cursor和事务上下文,不能每次start_transaction()新开一个——否则隔离性失效,还可能触发隐式提交
Spring 等框架里别依赖“自动重启事务”幻觉
MySQL 报 1213 后,事务已被强制回滚,但某些框架(如 Spring 的 @Transactional)会在 catch 异常后悄悄开启新事务,导致“前面的 SQL 回滚了,后面的却提交了”的诡异现象。
真实情况是:
- 死锁发生时,MySQL 已终止该事务,后续任何 SQL 都会报
Commands out of sync或静默失败 - Spring 不会帮你续上原事务,它只是开了个新事务——你得自己确保整个业务逻辑包裹在单次事务中,或显式控制重试边界
- 存储过程里用
DECLARE EXIT HANDLER FOR 1213封装重试可行,但要注意:不能嵌套事务,且外部不能已开启事务,否则START TRANSACTION会报错
最稳妥的做法,是把重试逻辑放在服务编排层,而不是 DAO 或 ORM 方法内部——那里只管执行,成败由上层决策。


















