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

只对 MySQL 1213 和 PostgreSQL 40P01 错误重试
死锁是瞬态竞争,不是程序 bug,但重试必须精准——只针对数据库明确声明的死锁错误。其他错误(如 sql.ErrNoRows、driver.ErrBadConn、约束冲突)重试只会掩盖问题或引发重复写入。
- MySQL:用
errors.As(err, &mysqlErr)断言mysql.MySQLError.Number == 1213,别用字符串匹配"Deadlock"—— 驱动升级或本地化可能让这个判断失效 - PostgreSQL:必须
errors.As(err, &pgErr)提取原始pgconn.PgError,再检查pgErr.Code == "40P01";GORM 等 ORM 会多层包装错误,errors.Unwrap可能不够,得一直As到底层 - SQLite:
database is locked不是死锁,是读锁未释放(*sql.Rows没defer rows.Close()),也不该按死锁逻辑重试
重试必须包裹整个事务,且每次都要 Rollback
事务中任意语句报死锁,整个事务已不可提交。只重试那条语句、不回滚,后续操作会 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还接着跑 —— 这破坏原子性,数据状态不可控
控制重试次数与退避策略
死锁重试不是越多越好。2–3 次足够覆盖绝大多数瞬态冲突;超过这个数,大概率是 SQL 设计问题(如访问顺序不一致、缺少索引),该查执行计划而不是加重试。
- 硬编码限制重试次数(如
maxRetries := 3),避免雪崩 - 每次重试前加指数退避 + 随机抖动:
time.Sleep(time.Millisecond * time.Duration(rand.Intn(50)+50)),防止重试请求打成尖峰 - 总耗时也要设上限:用
context.WithTimeout包裹整个重试循环,超时直接返回错误,不拖垮接口
优先从源头规避,而非依赖重试
重试是兜底,不是解药。真正降低死锁概率,得从 SQL 行为入手。
立即学习“go语言免费学习笔记(深入)”;
- 多行更新/删除必须排序:对 ID 切片调
sort.Ints(ids),或拼 SQL 时强制ORDER BY id—— 否则 A 更新 (100, 50),B 更新 (50, 100),立刻 AB-BA 死锁 -
SELECT FOR UPDATE必须走同一索引路径:如果有的走主键、有的走二级索引,即使顺序一致,也可能因执行计划不同锁住不同范围 - 拆分高冲突事务:把“扣库存 + 写订单 + 发消息”这种长事务拆成两个,比重试更治本
重试逻辑下沉到 service 层,handler 里只管传参和返回;而最容易被忽略的,是事务外的 db.Query 或 db.Exec 被误包进重试 —— 它们不属事务,重试只会重复写入。


















