死锁应仅对mysql错误码1213、postgresql错误码"40p01"重试,sqlite无标准死锁信号;重试须包裹整个事务,配合指数退避+随机抖动,且严格限制次数与总耗时。

只对确认的死锁错误重试,其他错误立即返回
死锁是瞬态资源竞争,重试大概率成功;但约束冲突、权限错误、连接中断等非死锁错误重试毫无意义,反而掩盖真实问题。必须严格区分错误类型,不能靠err != nil就重试。
- MySQL 死锁必须检查
mysql.MySQLError.Number == 1213,不能用字符串匹配"Deadlock"——本地化或驱动版本更新会让这个判断失效 - PostgreSQL 要用
pgconn.PgError.Code == "40P01",且需通过errors.As(err, &pgErr)提取原始错误,GORM 等 ORM 会包装错误,得先errors.Unwrap - SQLite 不提供标准死锁信号,
SQLITE_BUSY是忙等待,不是死锁,不应按死锁逻辑重试 - 任何非死锁错误(如
sql.ErrNoRows、driver.ErrBadConn、认证失败)都应直接返回,不进重试循环
重试必须包裹整个事务,不能只重试单条语句
事务中某条 Exec 报死锁时,事务已处于不可提交状态。若只重试那条语句而不回滚,后续操作会 panic 或报 sql: transaction has already been committed or rolled back。
- 重试逻辑起点是
db.Begin(),终点是tx.Commit()成功或明确判定非死锁错误 - 每次重试前必须调
tx.Rollback()(即使上一轮没显式 Commit),否则连接池可能卡住 - 不要在事务内做部分重试:比如第一条 UPDATE 失败后重试它,第二条 UPDATE 还接着执行——这会破坏事务原子性
- 如果业务允许,优先考虑拆分高冲突事务(如把“扣库存+写订单+发消息”拆成两个事务),比重试更治本
重试次数限制 + 随机抖动,避免重试洪峰
死锁重试不是越多越好。2–3 次足够覆盖绝大多数瞬态冲突;超过这个数,大概率是设计问题(如访问顺序不一致、缺少索引),该查 SQL 而不是加重试。
- 固定间隔(如每次都 sleep 1s)会导致大量请求在同一时刻重试,加剧锁争抢——这就是“重试风暴”
- 必须加 jitter:每次 sleep 时间 = 指数退避基础值 × (0.75 ~ 1.25) 随机因子,例如第 2 次重试在 1.5s~2.5s 之间随机等待
- 推荐初始间隔 100ms,最大不超过 2s,总重试耗时控制在 5s 内,避免拖慢接口响应
- 用
context.WithTimeout包裹整个重试块,确保超时后立刻退出,不残留 goroutine
初始化阶段重试和运行时重试是两回事
服务刚启动时数据库未就绪(如 K8s InitContainer 未完成),必须阻塞等待;而运行中连接断开,*sql.DB 连接池本身就能自动重建健康连接,无需手动干预。
- 启动阶段重试对象是
db.PingContext(),目标是让服务“等活”,不是“兜底”——失败就 panic 重启是反模式 - 运行时遇到死锁,才走事务重试逻辑;此时连接池大概率仍是健康的,
db.Ping()会成功,但事务里仍可能撞死锁 - 别在运行时反复调
db.Ping()判断连接状态——这是连接池该干的事;你只需专注事务逻辑和错误分类 -
db.SetConnMaxLifetime(10*time.Minute)和db.SetConnMaxIdleTime(5*time.Minute)才是防 stale 连接的关键,不是靠重试补救
SHOW ENGINE INNODB STATUS,而不是把重试次数从 3 改成 10。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











