gorm事务必须通过db.transaction()安全开启,回调中所有操作须使用tx而非db;返回nil自动提交,非nil error自动回滚;嵌套实为savepoint,不可手动begin或混用db。

事务必须手动开启和提交,GORM 不会自动包裹函数调用
GORM 的 DB.Transaction() 是唯一安全的事务入口,直接在普通 DB 实例上调用 Create()、Save() 等方法不会进入事务上下文。常见错误是误以为开启事务后所有后续操作都自动受控,实际只要没显式传入事务对象(即 *gorm.DB),就仍在默认连接上执行。
正确做法是把业务逻辑封装进回调函数,并确保所有数据库操作都使用传入的 tx 对象:
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&user).Error; err != nil {
return err // 触发回滚
}
if err := tx.Create(&profile).Error; err != nil {
return err // 触发回滚
}
return nil // 提交事务
})
- 回调返回
nil→ 自动Commit() - 回调返回非
nilerror → 自动Rollback() - 不要在回调里调用
db.Create()(用tx.Create()) - 回调内不可再嵌套
Transaction(),否则可能造成连接泄漏或死锁
嵌套事务不真正存在,GORM 用 SavePoint 模拟
GORM 不支持真正的嵌套事务(如 PostgreSQL 的 BEGIN TRANSACTION 嵌套),而是通过 SavePoint 实现“伪嵌套”。这意味着外层事务失败时,内层的 SavePoint 也会一并失效;但内层主动回滚到某个 SavePoint,不影响外层状态。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
手动控制 SavePoint 的典型场景是部分失败可接受的子流程:
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&order).Error; err != nil {
return err
}
// 子流程:发送通知,失败不阻断主事务
if err := tx.Exec("SAVEPOINT notify_sp").Error; err != nil {
return err
}
if err := sendNotification(); err != nil {
tx.Exec("ROLLBACK TO SAVEPOINT notify_sp")
}
return nil
})
- 手动
Exec("SAVEPOINT xxx")和ROLLBACK TO SAVEPOINT仅在支持该语法的数据库(PostgreSQL、MySQL 8.0+)生效 - SQLite 不支持
SAVEPOINT,GORM 会静默忽略,此时子流程失败仍会导致整个事务回滚 - 不要依赖
SavePoint实现业务级“可选提交”,应拆分为独立事务或应用层补偿
事务超时与连接泄漏:Context 必须传入且设限
未带 context.Context 的事务可能无限期等待锁,尤其在高并发或死锁场景下。GORM v1.23+ 支持在 Transaction() 中传入带超时的 ctx,但需注意:超时只作用于事务开启阶段(获取连接、启动事务),不控制回调函数执行时长。
- 用
context.WithTimeout(ctx, 5*time.Second)包裹整个Transaction()调用 - 若回调内有耗时操作(如 HTTP 请求、大文件处理),必须另行控制其超时,否则事务连接会被长期占用
- 事务结束后,
tx对象不可复用,也不可传递给 goroutine 异步使用,否则引发 panic 或数据错乱 - 日志中出现
"failed to get connection" or "context deadline exceeded",大概率是前序事务未结束或未释放
ReadCommitted 是默认隔离级别,跨事务读取需注意幻读
GORM 默认使用数据库驱动的默认隔离级别(多数为 ReadCommitted)。这意味着:事务 A 中两次 SELECT 同一范围数据,期间事务 B 插入并提交新行,事务 A 第二次查询可能看到新增行(幻读)。这不是 bug,而是隔离级别的固有行为。
- 需要强一致性时,应在事务开始时显式设置更高隔离级别:
tx.Session(&gorm.Session{PrepareStmt: true}).Exec("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE") - MySQL 中
SELECT ... FOR UPDATE可加行锁防止幻读,但需确保查询条件命中索引,否则升级为表锁 - 避免在事务中做长时计算或阻塞 I/O,否则锁持有时间过长,加剧竞争
- 使用
FirstOrInit()或FindOrCreate()时,其内部实现不含原子性保障,仍需包裹在事务中并手动加锁
Transaction() 自动解决。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










