只对MySQL 1213和PostgreSQL 40P01错误精准重试,须用errors.As提取底层错误码;重试必须包裹整个事务并显式Rollback;推荐2–3次指数退避重试;优先通过排序、索引、拆分事务从源头规避死锁。

只对 MySQL 1213 / PostgreSQL 40P01 错误做重试
死锁是数据库层的瞬态竞争,不是 Go 程序 bug,重试必须精准——仅针对明确声明的死锁错误码。其他错误(如 sql.ErrNoRows、driver.ErrBadConn、唯一约束冲突)重试会掩盖真实问题,甚至导致重复扣款或消息发两次。
MySQL 要用 errors.As(err, &mysqlErr) 检查 mysqlErr.Number == 1213;PostgreSQL 必须 errors.As(err, &pgErr) 提取底层 pgconn.PgError,再比对 pgErr.Code == "40P01"。GORM 等 ORM 会多层包装错误,errors.Unwrap 不够,得一直 As 到最底层。
别用字符串匹配:strings.Contains(err.Error(), "Deadlock") 在驱动升级、本地化语言变更后直接失效。
重试必须包裹整个事务,且每次都要显式 Rollback
事务中任意语句触发死锁,整个事务已不可提交。只重试那条 UPDATE、不回滚,后续操作会 panic 报 sql: transaction has already been committed or rolled back。
重试逻辑起点是 db.BeginTx(),终点是 tx.Commit() 成功;中间任何失败都必须调 tx.Rollback()。即使上一轮没走到 Commit,重试前也得先 tx.Rollback()——否则连接池里的连接卡住,最终触发 fatal error: all goroutines are asleep - deadlock!。
常见错误:
- 在事务内对某条语句局部重试(比如 UPDATE 失败后只重试它),后面 INSERT 还照常执行 → 破坏原子性
- 用
defer tx.Rollback()代替显式判断 → panic 后 defer 可能不执行,连接永远泄漏 - HTTP handler 里直接写重试循环 → 业务逻辑和错误恢复混在一起,难以复用和测试
控制重试次数与退避策略,设总超时
2–3 次重试足够覆盖绝大多数瞬态冲突。超过这个数,大概率是 SQL 设计缺陷:访问顺序不一致、缺少索引、事务过大。继续加重试只会放大雪崩风险。
硬编码 maxRetries := 3,别用配置项动态调整——防止线上误配成 10 次引发连锁超时。
每次重试前加指数退避 + 随机抖动:time.Sleep(time.Millisecond * time.Duration(rand.Intn(50)+50)),避免重试请求打成尖峰。
整个重试循环必须用 context.WithTimeout 包裹,例如 3 秒总上限,超时直接返回错误,不拖垮接口。
优先从 SQL 行为源头规避死锁
重试是兜底,不是解药。真正降低死锁概率,靠的是让多个事务按相同顺序访问数据。
多行更新/删除前必须排序:sort.Ints(ids) 或 SQL 中强制 ORDER BY id。否则事务 A 更新 (100, 50),B 更新 (50, 100),立刻 AB-BA 死锁。
SELECT FOR UPDATE 必须走同一索引路径——如果 WHERE 条件没走索引,可能升级为表锁,大幅提高死锁概率。
GORM 中启用悲观锁要显式指定强度:Clauses(clause.Locking{Strength: "UPDATE"}),并配合 ORDER BY 使用;批量操作慎用 Save(),无主键时可能隐式全表扫描加锁。
长事务混合读写是高危模式。把 SELECT 和 UPDATE 拆成两个短事务,或统一改用 SELECT ... FOR UPDATE 提前锁定,避免中间穿插非锁定读。


















