db.transaction回调内禁止起goroutine,因*gorm.db绑定的sql.tx非并发安全,跨协程操作会触发事务已提交/回滚错误或丢数据;所有db操作须同步完成,异步任务需提取参数后在事务提交后再执行。

db.Transaction 回调里不能起 goroutine
事务对象 tx 不是并发安全的,一旦在 db.Transaction 的回调函数里启动 goroutine 并在里面调用 tx.Create 或其他 DB 操作,大概率触发 sql: Transaction has already been committed or rolled back,或者静默丢数据。
- 根本原因:GORM 的事务实例(
*gorm.DB)底层绑定的是sql.Tx,而sql.Tx明确不支持跨 goroutine 使用 - 典型错误写法:
db.Transaction(func(tx *gorm.DB) error { go func() { tx.Create(&Order{...}) // ❌ 危险! }() return nil }) - 正确做法:所有 DB 操作必须在回调内同步完成;异步任务(如发消息、写日志)应提取参数后,在事务提交成功后再由外部协程处理
事务内禁止传递 context 跨协程使用
你在事务回调里拿到的 context.Context(比如从 gin.Context 提取的)一旦被传进 goroutine,就可能在事务结束、连接释放后还被访问,导致 context canceled 或 panic。
-
gin.Context是请求级生命周期,和事务生命周期不重合;事务结束 ≠ 请求结束,但gin.Context可能已被回收 - 不要这样写:
db.Transaction(func(tx *gorm.DB) error { ctx := c.Request.Context() go doAsync(ctx, tx) // ❌ ctx 和 tx 都不可靠 return nil }) - 如果真要异步,先拷贝必要数据(如用户 ID、订单号),再用干净的
context.Background()启动 goroutine
嵌套事务不是真嵌套,Savepoint 名称冲突会报错
GORM 的 tx.Transaction(即嵌套调用)本质是 SAVEPOINT,不是独立事务。它依赖数据库驱动支持,且名称管理很脆弱。
- PostgreSQL 和 MySQL 5.7+ 支持
SAVEPOINT,但 SQLite 默认关闭,需确认驱动配置启用 - 错误现象:
ERROR: savepoint name already exists—— 多层嵌套时若未手动指定唯一 savepoint 名,GORM 默认用固定名gorm_savepoint_1,重复调用就炸 - 避免方式:不用嵌套,把逻辑压平;非要嵌套,显式传入唯一名称:
tx.SavePoint("sp_order_create").Create(...) - 关键认知:外层事务失败,所有 savepoint 自动失效;内层失败只回滚到 savepoint,但不会“隔离”错误——这不是 ACID 嵌套,只是回滚锚点
别在中间件或 handler 里直接开事务协程
常见误区:想“异步执行事务提高吞吐”,于是在 Gin handler 里直接 go db.Transaction(...)。这会导致事务脱离请求上下文,无法感知超时、无法绑定 trace ID、无法统一 rollback 策略。
- 事务必须与业务逻辑生命周期对齐:下单 → 扣库存+写订单+记流水,这三步必须在一个事务里原子完成,不能拆到不同 goroutine
- 真正需要并发的场景(如批量导入),应按批次分事务,每批次一个
db.Transaction,而不是一个事务里塞一堆 goroutine - 性能瓶颈通常不在事务本身,而在连接池耗尽或锁竞争;优先调优
SetMaxOpenConns和 SQL 索引,而不是强行并发化事务
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











