事务回滚无效主因是transaction()套娃、混用db与tx、autocommit启用;应避免嵌套transaction(),改用tx.begin()配合rollbackto(),sql server需显式设autocommit=false,并手动处理panic与错误传播。

事务回滚无效,90% 是因为用了 Transaction() 套娃、混用 db 和 tx、或底层数据库驱动启用了 autocommit。 它不是 GORM 的 bug,而是对事务模型理解偏差导致的控制权丢失。
别在 Transaction() 里再调 Transaction()
这是最常见也最隐蔽的失效点。GORM 的 Transaction() 是“一次性闭包”,内部自动 Begin() + Commit()/Rollback(),且不共享上下文。
- 外层
Transaction()启动一个事务,拿到tx1 - 若你在其闭包内又调一次
Transaction(),它会新建连接或复用但脱离tx1上下文,变成独立事务tx2 -
tx2提交后数据立刻落库,后续tx1.Rollback()对它完全无效
正确做法:需要嵌套逻辑时,必须用 tx1.Begin(),而不是二次 Transaction()。
MySQL 嵌套事务本质是 Savepoint,回滚必须用 RollbackTo() 或 tx2.Rollback()
MySQL 不支持真正嵌套事务,tx1.Begin() 实际是创建一个命名 savepoint(如 "sp_1"),tx2.Rollback() 等价于回滚到该点,不影响 tx1 已执行的操作。
- 子事务失败时,不要直接
tx1.Rollback()—— 这会干掉全部操作 - 应调
tx2.Rollback()局部回滚,保留tx1中已完成的步骤(如日志写入) - 若需精确控制 savepoint 名称,可先
tx1.SavePoint("sp_name"),再tx1.RollbackTo("sp_name") - 最终仍由
tx1.Commit()或tx1.Rollback()决定整体成败
SQL Server 必须关掉 autocommit=false,否则 Rollback() 形同虚设
go-mssqldb 驱动默认 autocommit=true,每条语句都会被自动包装成独立事务并立即提交,Begin() / Rollback() 完全失效。
- 连接字符串中必须显式加
autocommit=false:"server=localhost;database=test;user id=sa;password=xxx;autocommit=false" -
gorp或GORM的Begin()不会修改该连接级配置,只封装*sql.Tx - 使用连接池时,
autocommit=false会被透传到每个新连接,无需额外处理 - 不要依赖 T-SQL 的
SET IMPLICIT_TRANSACTIONS ON,Go 驱动层不保证兼容性
panic 不触发自动回滚,手写 Begin() 必须自己兜住
Transaction() 闭包能 recover panic 并回滚;但手写 tx := db.Begin() 链式事务不会,panic 会导致连接卡死、后续请求 hang 住。
- 绝不能只靠
defer tx.Rollback()—— 成功时它会误回滚 - 每个子操作后必须检查
err,按需调tx2.Rollback()或tx1.Rollback() - 外层
tx1.Commit()前,务必确认所有子事务返回nilerror - 若真要防 panic,得手动加
defer func() { if r := recover(); r != nil { tx1.Rollback() } }()
最容易被忽略的是:事务边界和错误传播必须严格一对一。一个 tx 对应一组操作,所有 Create/Update 必须用它,不能穿插 db.Create();而每个子事务的 error 要原路透出,不能被中间层吞掉或转成其他 error 类型。











