GORM 的 db.Transaction 不支持自动重试,仅回滚并退出;手动重试需限定错误类型、次数(建议≤3次)、指数退避,并每次新建事务。

db.Transaction 本身不支持自动重试
GORM 的 db.Transaction 函数只负责捕获 panic 并回滚,它不会重试、不感知业务错误(比如校验失败、库存不足)、也不重试网络超时或死锁。你写 db.Transaction(func(tx *gorm.DB) error { ... }),一旦函数返回非 nil error 或 panic,事务就回滚并退出——仅此而已。
手动实现重试必须控制重试次数和错误类型
自动重试不是“包一层 for 循环”就行,重点是:什么错误值得重试?重几次够?怎么避免重复执行副作用?
- 只对可重试的底层错误重试:如
driver.ErrBadConn、sql.ErrTxDone、mysql.ErrDeadlock(MySQL)、"deadlock detected"(PostgreSQL) - 绝对不要重试业务逻辑错误:比如
errInventoryShortage、errUserNotFound,重试只会让问题更糟 - 重试前必须清空状态:每次重试都应新建事务,不能复用已失败的
tx实例;否则会 panic 报"invalid transaction" - 建议上限 3 次,配合指数退避(如 10ms → 30ms → 100ms),避免雪崩
典型死锁重试示例(带退避)
func transferWithRetry(db *gorm.DB, fromID, toID uint, amount int) error {
var err error
for i := 0; i < 3; i++ {
err = db.Transaction(func(tx *gorm.DB) error {
// 注意:所有操作必须用 tx,不能混用 db
var from, to Account
if err := tx.Where("id = ?", fromID).ForUpdate().First(&from).Error; err != nil {
return err
}
if err := tx.Where("id = ?", toID).ForUpdate().First(&to).Error; err != nil {
return err
}
if from.Balance < amount {
return errors.New("insufficient balance")
}
from.Balance -= amount
to.Balance += amount
if err := tx.Save(&from).Error; err != nil {
return err
}
return tx.Save(&to).Error
})
<pre class="brush:php;toolbar:false;"> if err == nil {
return nil
}
// 只对死锁错误重试,其他错误直接返回
if isDeadlockError(err) {
if i < 2 {
time.Sleep(time.Duration(math.Pow(10, float64(i+1))) * time.Millisecond)
continue
}
}
break
}
return err}
func isDeadlockError(err error) bool { return strings.Contains(err.Error(), "deadlock") || strings.Contains(err.Error(), "ErrDeadlock") || err == mysql.ErrDeadlock }
容易被忽略的关键点
重试逻辑里最常漏掉的是:事务外的副作用没做幂等防护。比如你在事务成功后发 MQ 消息或调 HTTP 回调,重试会导致消息重复、通知发两次。正确做法是把这类 I/O 移到 tx.AfterCommit() 钩子里,并确保回调本身支持幂等(例如带上唯一 business_id)。另外,tx.AfterCommit() 只对当前事务实例生效,如果重试时用了新 tx,旧钩子就丢了——所以钩子注册必须放在每次事务函数体内,不能提前提前定义。


















