必须使用BeginTx(ctx, opts)开启事务并立即绑定未超时的context,事务生命周期须严格依附该context;超时后需显式检查ctx.Err()并手动Rollback,否则连接泄漏、锁不释放、数据不一致。

事务开始时必须绑定上下文,否则超时信号无法传递
Go原生database/sql的Begin()不接受context.Context,但BeginTx(ctx, opts)才是事务与上下文联动的唯一入口。用Begin()开启事务后,后续所有QueryContext、ExecContext调用虽然能响应超时,但事务本身仍处于 open 状态——哪怕上下文已过期,tx.Commit()或tx.Rollback()仍可执行,数据一致性完全失控。
正确做法是:从第一步就用db.BeginTx(ctx, nil),并确保该ctx携带合理超时(如context.WithTimeout(ctx, 30*time.Second))。事务生命周期必须严格依附于该上下文的生命周期。
- 不要在事务外创建超时上下文,再传给单个查询;要让事务本身“生于上下文、死于上下文”
-
BeginTx返回的*sql.Tx不持有上下文引用,但其底层驱动会监听ctx.Done()用于中断当前语句 - 若业务逻辑中需嵌套子事务(如Savepoint),子操作也必须复用同一
ctx,不可新建
GORM中DefaultTransactionTimeout不是银弹,操作级WithContext优先级更高
GORM的DefaultTransactionTimeout只影响未显式传入context的事务操作。一旦你调用db.WithContext(ctx).Transaction(...)或db.WithContext(ctx).Begin(),该ctx的超时设置将完全覆盖全局配置。这意味着:全局设了60秒,但某次转账操作只给了5秒WithTimeout,那它真就只能活5秒。
常见误判是以为设了全局超时就一劳永逸。实际上,GORM在callbacks/query.go里把db.Statement.Context直接透传给驱动,根本不会 fallback 到DefaultTransactionTimeout。
立即学习“go语言免费学习笔记(深入)”;
- 高风险操作(如批量更新+发消息)建议始终用
WithContext显式控制,不依赖默认值 -
DefaultTransactionTimeout仅作为兜底,防止开发者完全忘记传ctx - 注意
Transaction方法内部会自动defer tx.Rollback(),但回滚触发条件仍是你的ctx是否已取消,而非超时时间本身
GoFrame事务Ctx()方法必须在Begin后立即调用,延迟绑定无效
GoFrame的TX.Ctx(ctx)不是构造时绑定,而是运行时“挂载”。如果你写成:
tx, _ := db.Begin()
// 中间做了其他事(比如查缓存、调第三方API)
tx.Ctx(ctx) // ❌ 此时可能已超时,Ctx()只是设置字段,不重置状态
tx.Exec("UPDATE ...")
这段代码大概率失败——因为tx.Ctx(ctx)只是把ctx存进结构体字段,不校验ctx.Err(),也不重置任何内部计时器。当tx.Exec执行时,若ctx早已Done(),驱动层直接返回context.DeadlineExceeded,而事务仍开着。
- 必须在
Begin()返回后立刻调用Ctx(),且ctx应是刚创建的、未被取消的实例 - 推荐写法:
tx := db.Begin().Ctx(ctx),链式调用避免中间插入无关逻辑 - GoFrame的
transactionTimeout配置项只作用于Transaction()函数,对Begin()+ 手动Commit()流程无约束
超时发生后,手动Rollback不是可选动作,而是强制清理步骤
无论用database/sql、GORM还是GoFrame,上下文超时只会中断当前SQL执行并返回错误,绝不会自动Rollback()。这是最常被忽略的致命点:事务卡在open状态,连接被占用,锁未释放,后续请求全被阻塞。
正确清理模式必须包含显式回滚判断:
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() {
if ctx.Err() != nil { // ⚠️ 必须检查ctx.Err(),不能只看err
tx.Rollback()
}
}()
// ... 执行业务SQL
if err := tx.Commit(); err != nil {
if ctx.Err() == nil { // 只有非超时错误才需记录
log.Error(err)
}
return err
}
- 不能只靠
defer tx.Rollback()——它会在函数退出时无条件执行,可能把本该成功的事务也回滚了 - 必须区分错误类型:
ctx.Err()表示超时/取消,tx.Commit()返回的err可能是唯一约束冲突等业务错误 - 在HTTP handler中,还要注意:客户端断开连接(
http.CloseNotifier废弃后靠ctx.Done())同样触发此路径,必须统一处理
BeginTx、没立刻绑定Ctx()、没检查ctx.Err()就提交——都可能导致连接泄漏、死锁或数据不一致。这些点不在文档首页,但每个都在生产环境真实炸过。


















