绝大多数业务写操作必须用 db.Transaction;因 db.Begin() 返回新事务实例,混用 db.Update() 会绕过事务直连数据库,导致数据不一致,且 tx 提交/回滚后不可复用。

绝大多数业务写操作必须用 db.Transaction,而不是手写 db.Begin() + tx.Commit();否则极大概率漏回滚、卡连接、数据不一致。
为什么 db.Begin() 后混用 db.Update() 就失效了
因为 db.Begin() 返回的是一个全新的 *gorm.DB 实例(带事务上下文),原始 db 对象仍是无事务的普通连接。所有 CRUD 操作若继续走 db,就等于绕过事务直连数据库。
- ✅ 正确:用返回的
tx调用tx.Model(&User{}).Where(...).Update(...) - ❌ 危险:在
tx事务中调用db.Model(...).Update(...)—— 这条更新已不可逆落库,破坏原子性 - ⚠️ 注意:
tx提交或回滚后不可复用,再调tx.Update()会 panic 报"invalid transaction"
db.Transaction 中如何安全做条件更新
db.Transaction 是唯一推荐的入口,它自动管理连接生命周期、panic 回滚,但不自动提交、不校验逻辑错误——你得自己兜底。
- 必须显式调用
tx.Commit(),仅return nil不会提交,事务会被丢弃 - 分支逻辑里漏
return err(比如if x { return } else { ... })会导致静默失败 - 条件更新优先用
tx.Model(&User{}).Where("id = ? AND status = ?", id, "active").Updates(map[string]interface{}{"status": "done", "updated_at": time.Now()}),避免零值被忽略 - 如果要更新零值字段(如
Active: false),别用 struct 形式Updates(user),改用 map 或加Select("active")
嵌套条件更新 + HTTP 调用怎么不破事务
HTTP 请求、缓存写入、日志上报等 I/O 操作严禁进事务体——它们可能超时、阻塞、失败,直接拖垮连接池。
- 把外部调用移出
db.Transaction函数体,改用tx.AfterCommit(func() { http.Post(...) }) -
tx.AfterCommit()必须在事务函数体内注册,且只对当前tx生效;传错对象(如传db或新tx.WithContext())钩子就失效 - 需要强一致性时,别依赖
AfterCommit,改用本地消息表 + 定时任务补偿 - SAVEPOINT 不是子事务,
tx.Begin()在已有事务内只是模拟,inner.RollbackTo("sp")只回滚到保存点,外层已执行的更新仍生效
最常被忽略的点:事务里没写 tx.Commit(),或者用了 db.Save() 而非 tx.Save(),这两处一错,整个原子性就崩了。别信“写了 Transaction 就自动兜底”,GORM 只拦 panic,不拦逻辑疏漏。


















