GORM手动事务必须以db.Begin()为唯一入口,返回的tx实例须全程使用,混用db将绕过事务;需显式调用Commit()或Rollback(),推荐defer+recover防panic导致连接泄漏;嵌套逻辑应使用SavePoint而非重复Begin()。

手动调用 Begin() 是唯一入口
GORM 不支持通过配置或全局开关自动进入事务模式,Begin() 是开启手动事务的强制起点。它返回一个带事务上下文的 *gorm.DB 实例(通常叫 tx),后续所有操作必须使用这个实例,否则会绕过事务直接写入数据库。
常见错误现象:在 tx 中误用了原始 db 变量,导致部分操作未被事务包裹,回滚时数据不一致。
-
tx := db.Begin()后,所有Create/Update/Delete/Model等都必须基于tx - 不能混用
db.Create()和tx.Update(),哪怕只有一处,事务完整性就失效 - 如果需要查询非事务数据(比如读取配置表),应另起独立
db连接,而非复用tx
Rollback() 和 Commit() 必须显式配对
手动事务不会自动提交或回滚,GORM 1.20 虽然能捕获回调内 panic 并转为 error,但对顶层手动流程无影响——你得自己决定何时调用 Rollback() 或 Commit()。
容易踩的坑:忘记 defer 或提前 return 导致事务既没提交也没回滚,连接可能长期占用,甚至引发锁等待超时。
- 推荐用
defer+recover()捕获 panic 后回滚:defer func() { if r := recover(); r != nil { tx.Rollback() } }() - 每个分支路径都要有明确的
tx.Commit()或tx.Rollback(),不能只在 if 分支里写Rollback而漏掉 else - 不要依赖函数返回值自动判断:GORM 不会因 error 自动回滚,
tx.Create(...).Error != nil只是提示,仍需你主动调用Rollback()
嵌套事务要用 SavePoint,不是重复 Begin()
连续调用两次 db.Begin() 不会产生嵌套事务,第二次调用实际会报错或覆盖前一个事务上下文(取决于驱动和 GORM 版本)。真要支持子事务回滚而不影响外层,必须用 SavePoint。
使用场景:比如主订单创建失败要整体回滚,但其中优惠券核销失败只应局部回滚,不影响订单主体。
- 设置保存点:
tx.SavePoint("coupon_step"),返回新事务句柄(仍是*gorm.DB) - 回滚到该点:
tx.RollbackTo("coupon_step"),不会影响之前的操作 -
SavePoint名称必须唯一,重复名称会导致行为不可预期(如静默覆盖) - MySQL 支持良好,SQLite 需开启 WAL 模式,PostgreSQL 全支持;但某些云数据库代理层可能不透传 SavePoint 命令
事务生命周期与连接池强相关
Begin() 会从连接池中取出一个空闲连接并独占它,直到 Commit() 或 Rollback() 被调用,连接才归还。这意味着事务持续时间越长,连接池压力越大,尤其在高并发下容易触发 connection timeout 或 too many connections 错误。
性能影响常被忽略:一次事务内执行 10 次小更新,比开 10 次短事务更耗连接资源,也更容易被锁阻塞。
- 避免在事务中做 HTTP 请求、文件 IO、复杂计算等耗时操作
- 事务内只做必要 DB 操作,非 DB 逻辑尽量前置或后置
- 监控
tx.Statement.ConnPool状态可辅助排查连接泄漏,但生产环境不建议直接访问该字段 - 超时控制靠数据库层(如 MySQL 的
wait_timeout)或应用层 context.WithTimeout 包裹整个事务逻辑块
Begin/Commit,而是确保所有路径都被覆盖、所有连接都被释放、所有保存点命名不冲突——这些细节一旦漏掉,问题往往在线上压测或大促期间才爆发。


















