go 必须通过 errors.as 提取 mysql.mysqlerror 并判断 number == 1213 来精准识别 mysql 死锁;重试需包裹整个事务,每次循环前调用 tx.rollback(),限制 2–3 次指数退避重试,并开启 innodb_print_all_deadlocks=on 记录死锁日志。

Go 本身不检测 MySQL 死锁,必须靠捕获 mysql.MySQLError.Number == 1213 这一明确信号来触发重试;其他错误(如连接中断、约束冲突)盲目重试只会掩盖问题或引发重复写入。
如何精准识别 MySQL 死锁错误
不能用 strings.Contains(err.Error(), "Deadlock") —— 驱动版本更新或中文环境会让这个判断失效。必须用 errors.As 提取底层错误类型。
- 导入
github.com/go-sql-driver/mysql - 检查前先
errors.Unwrap多层包装(尤其用了 GORM 时) - 断言成功后只比对
mysqlErr.Number == 1213,这是唯一稳定标识 - SQLite 的
database is locked不是死锁,是*sql.Rows没defer rows.Close()导致的读锁滞留,别混为一谈
重试必须包裹整个事务,且每次都要 Rollback
事务中某条 Exec 报 1213,该事务已不可提交。只重试那条语句、不回滚,后续操作会 panic 报 sql: transaction has already been committed or rolled back。
MySQL 9.6.0是面向Linux平台的2026年创新版本,核心架构迎来重大革新。其将外键约束与级联操作从InnoDB引擎层上移至SQL层,确保所有数据变更均被完整记录至Binlog,彻底解决了CDC(变更数据捕获)与主从复制中的数据不一致难题。此外,该版本引入container_aware启动选项以原生适配容器环境,并对审计日志进行了组件化重构,为追求极致数据一致性与云原生体验的开发者提供了全新选择。
- 重试起点是
db.BeginTx(),终点是tx.Commit()成功 - 每次循环开头都得调一次
tx.Rollback(),哪怕上一轮根本没走到Commit—— 否则连接池可能卡住 - 禁止在事务内做“局部重试”:比如
UPDATE失败后只重试它,后面INSERT还接着执行 - 如果业务允许,优先把高冲突操作拆成两个事务(如“扣库存”和“写订单”分离),比重试更治本
控制重试次数、退避与超时边界
重试不是越多越好。2–3 次足够覆盖绝大多数瞬态冲突;超过这个数,大概率是 SQL 设计问题(如访问顺序不一致、缺少索引),该查执行计划而不是加重试。
- 硬编码限制
maxRetries := 3,避免雪崩 - 每次等待时间 = 指数退避 × 随机因子(例如第 2 次在 150ms~250ms 之间随机)
- 用
context.WithTimeout包裹整个重试块,总耗时建议 ≤ 5s,超时立刻退出 - 固定间隔(如每次都
time.Sleep(1 * time.Second))会引发重试洪峰,加剧锁争抢
最容易被忽略的一点:死锁日志默认不落盘。MySQL 必须显式开启 innodb_print_all_deadlocks = ON,否则线上出问题根本抓不到现场;而 Go 层报的 context deadline exceeded 或 driver: bad connection,很可能就是死锁被数据库回滚后伪装出来的假象。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










