Go实现数据库自动重试需精准识别死锁错误(MySQL 1213、PostgreSQL 40P01)、严格包裹整个事务、使用带抖动的指数退避,并对非死锁错误立即返回。

database/sql 本身不提供自动重试,Go语言中实现数据库查询的自动重试,核心不是“结合语言学习”,而是精准识别死锁错误 + 严格包裹事务 + 指数退避加抖动。所有所谓“智能学习”在生产数据库场景中既不可靠、也不必要——死锁就是死锁,它只由特定错误码定义,不是靠模型猜出来的。
怎么判断是不是死锁错误(MySQL/PostgreSQL/SQLite)
不能用字符串匹配,必须提取底层驱动错误:
- MySQL:
errors.As(err, &mysqlErr)后检查mysqlErr.Number == 1213,不是"Deadlock" - PostgreSQL:
errors.As(err, &pgErr)后检查pgErr.Code == "40P01",不是err.Error()里搜"deadlock" - SQLite:
SQLITE_BUSY是忙等待,不是死锁,sqlite3.ErrBusy不该进重试逻辑
其他任何错误——比如 sql.ErrNoRows、driver.ErrBadConn、context.DeadlineExceeded、约束冲突(MySQL 1062)、权限错误(MySQL 1045)——都立即返回,重试只会掩盖问题。
为什么重试必须包裹整个事务,不能只重试某条语句
事务中一旦某条 Exec 或 Query 报死锁,*sql.Tx 就已进入不可提交状态。此时若不 Rollback() 就继续执行后续语句,会 panic:sql: transaction has already been committed or rolled back。
立即学习“go语言免费学习笔记(深入)”;
- 重试起点只能是
db.Begin() - 每次循环开头必须调
tx.Rollback()(即使上一轮没显式Commit,也要防卡住连接) - 不能在事务内做“局部重试”:比如第一条 UPDATE 失败后重试它,第二条 INSERT 还照常执行——这破坏原子性,数据可能半写入
用 backoff/v4 实现带 jitter 的安全重试
手写 for+time.Sleep 极易出错:goroutine 泄漏、忽略 ctx.Done()、固定间隔引发重试风暴。直接用成熟库:
bo := &backoff.ExponentialBackOff{
InitialInterval: 100 * time.Millisecond,
MaxInterval: 2 * time.Second,
MaxElapsedTime: 5 * time.Second,
Clock: backoff.SystemClock,
}
bo = backoff.WithJitter(bo)
<p>err := backoff.Retry(func() error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback() // 确保清理</p><pre class="brush:php;toolbar:false;">_, err = tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, userID)
if err != nil {
var mysqlErr *mysql.MySQLError
var pgErr *pgconn.PgError
switch {
case errors.As(err, &mysqlErr) && mysqlErr.Number == 1213:
return err // 让 backoff 继续重试
case errors.As(err, &pgErr) && pgErr.Code == "40P01":
return err
default:
return backoff.Permanent(err) // 非死锁错误,不再重试
}
}
return tx.Commit()}, backoff.WithContext(bo, ctx))
注意:backoff.Permanent(err) 是关键,它告诉 backoff.Retry “别再试了”,避免把约束错误也当死锁反复冲。
重试次数和耗时必须硬限制
2–3 次重试足够覆盖绝大多数瞬态死锁;超过这个数,大概率是业务逻辑问题:
- 访问顺序不一致(A→B vs B→A)
- 缺少索引导致锁范围过大
- 事务粒度太粗(比如把扣库存、写订单、发消息全塞一个事务)
与其加更多重试,不如先拆事务或加索引。另外,MaxElapsedTime 必须设(推荐 ≤5s),否则慢查询+重试会让接口超时雪崩。


















