必须显式使用db.BeginTx(ctx, opts)开启事务并手动Rollback(),否则超时后连接卡住、锁不释放;db.Transaction()传ctx无效,因其默认调用无context的db.Begin()。

事务超时不能靠 db.WithContext(ctx).Transaction() 包一层就完事——必须从 BeginTx 开始绑定,且超时后必须手动 Rollback(),否则连接卡住、锁不释放、数据状态不可信。
为什么 db.Transaction() 传 context 还是可能失效
常见误判:以为 db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { ... }) 就能自动响应超时。实际上 GORM 的 Transaction() 内部默认调用的是 db.Begin()(不带 context),而非 db.BeginTx(ctx, opts)。这意味着:
- 上下文超时信号根本没传进事务底层,
ctx.Done()被忽略 - 即使
ctx.Err()已为context.DeadlineExceeded,后续tx.Commit()或tx.Rollback()仍可执行,但结果不可靠 - 事务持有的数据库连接和行锁不会被驱动层主动中断
正确开启事务并绑定 context 的唯一方式
必须显式使用 BeginTx,且该 ctx 必须携带合理超时(不能是 context.Background() 或未设 timeout 的请求 context):
- 用
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)派生子 context,defer cancel()不可省略 - 调用
tx, err := db.Session(&gorm.Session{Context: ctx}).Begin()—— 注意:GORM v2 中Session().Begin()会透传Context到BeginTx,但前提是db本身未被预设为无 context 模式 - 更稳妥写法(绕过 GORM 封装):
sqlTx, err := db.Statement.ConnPool.(*sql.DB).BeginTx(ctx, nil),再用db.Session(&gorm.Session{NewDB: true}).WithContext(ctx).Session(&gorm.Session{Context: ctx}).Debug()绑定到 GORM 实例 - 所有子查询、关联更新、Savepoint 都必须复用同一个
ctx,不能在事务函数里新建context.Background()
超时发生后必须立即 Rollback,不能依赖 defer
context 超时后,ctx.Err() 变为非 nil,但事务对象 tx 仍是 open 状态。此时:
-
defer tx.Rollback()无效——它只在函数 return 时触发,而超时可能发生在中间某条 SQL 执行中 - 必须在每次关键操作(如
tx.Create()、tx.First())后检查:if errors.Is(err, context.DeadlineExceeded) || errors.Is(ctx.Err(), context.DeadlineExceeded) { tx.Rollback(); return err } - GORM 的
DefaultTransactionTimeout完全不生效——只要显式传了ctx,它就被彻底覆盖 - HTTP handler 中起 goroutine 执行事务?危险!
c.Request.Context()在 response 写完后立刻 cancel,野生 goroutine 几乎必收context canceled
异步或长耗时事务怎么处理
如果事务逻辑本身需要超过 HTTP 请求生命周期(比如生成报表、批量导出),就不能复用请求 context:
- 改用
context.Background()+ 显式 timeout:ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) - 务必记录 cancel 函数的调用点,避免 timer 泄漏;建议配合结构体封装生命周期管理
- 更健壮的做法是剥离事务逻辑,接入任务队列(如 Asynq),由独立 worker 消费,用任务级 context 控制
- 若必须嵌套子事务(如 Savepoint),子操作的
tx.SavePoint("sp1")和tx.RollbackTo("sp1")也必须复用同一ctx,否则超时传播断裂
最易被忽略的一点:GORM 事务的“超时”不是 SQL 层面单语句超时,而是整个事务生命周期的控制权移交。一旦 context 取消,你得立刻放弃当前所有状态、释放锁、关闭连接——没有“部分提交”这回事。


















