事务需手动开启并显式传递,gorm 的 begin() 不绑定 iris 请求生命周期;db.transaction() 不支持真正嵌套,多表操作须按序执行并逐个校验错误,外部调用不可纳入事务。

事务必须手动开启,GORM 的 Begin() 不等于 Iris 的请求生命周期
Iris 本身不提供事务抽象,事务控制完全交由底层 ORM(如 GORM)处理。常见误区是以为在中间件或路由 handler 里调用 db.Begin() 就自动覆盖整个请求——其实它只作用于当前 DB 实例,且不会跨 goroutine 自动传播。你得显式把 *gorm.DB 传给后续逻辑,或通过 ctx.Values().Set() 挂载,但后者需自行保证并发安全。
典型错误现象:db.Transaction() 内部报错回滚了,但上层业务代码仍继续执行并尝试 Save(),结果写入了脏数据;或者多个协程共用一个 *gorm.DB 实例,事务状态混乱。
- 事务应紧贴业务边界:比如「下单」操作含订单表、库存表、用户余额表三张写入,就应在 handler 开头
tx := db.Begin(),结尾统一tx.Commit()或tx.Rollback() - 别依赖 defer:defer 在函数 return 后才执行,若中间 panic 或提前 return,可能漏掉 Rollback
- GORM v2 默认开启 prepare stmt,事务内多次 exec 可能触发连接复用问题,建议在
gorm.Config中设PrepareStmt: false(尤其 MySQL 8.0+)
db.Transaction() 看似简洁,但嵌套调用会丢事务上下文
db.Transaction() 是 GORM 提供的快捷封装,内部自动 Begin/Commit/Rollback,但它返回的是新事务 DB 实例,原 db 不受影响。如果你在事务函数里又调用了另一个也用 db.Transaction() 的服务方法,第二层会新建事务,和第一层无关——即「伪嵌套」,无法实现真正的级联回滚。
使用场景:适合单层、无子服务调用的简单事务,比如仅更新一张订单表的状态。
- 正确做法:把事务实例
tx显式传入所有需要写库的函数,例如createOrder(tx, ...)、deductStock(tx, ...) - 参数差异:
db.Transaction(func(tx *gorm.DB) error { ... })的闭包内不能直接用外部db,所有操作必须走tx - 若需复用已有 service 层方法,改写其签名,接受
*gorm.DB参数而非硬编码GetDB()
多表级联更新失败时,Rollback() 必须在 defer 外显式判断
很多人写成 defer tx.Rollback() 然后在后面 tx.Create(...),以为只要没 Commit() 就安全——但 GORM 的 Create()、Save() 等方法失败时不会自动触发 rollback,defer 会照常执行,导致事务被强制回滚,掩盖真实错误原因。
常见错误现象:库存扣减失败,日志只打印 “transaction rolled back”,但没说明是 SQL 错误、约束冲突还是超卖。
- 必须检查每个写操作的 error:
if err := tx.Create(&order).Error; err != nil { tx.Rollback(); return err } - 级联更新顺序很重要:先扣库存(可能失败),再写订单(依赖库存成功),最后更新用户余额(依赖订单 ID);顺序颠倒会导致回滚后状态不一致
- MySQL 下外键约束错误(如子表插入引用不存在的父表 ID)会直接让
Create()返回foreign key constraint fails,这类错误必须捕获并透出,不能静默
事务中调用 HTTP API 或发消息,失败后无法原子回滚
这是最容易被忽略的复杂点:事务只能保证数据库状态一致,但如果你在 tx.Commit() 前调用了第三方支付接口、发了 Kafka 消息、或写了 Redis 缓存,这些操作无法随 DB 事务一起回滚。一旦 DB 提交成功而外部调用失败,系统就会进入最终不一致状态。
解决思路不是“避免调用”,而是设计补偿流程:
- DB 提交后,再异步触发外部动作(如写入一张
outbox表记录待发消息,由单独消费者投递) - 或采用 Saga 模式:把「创建订单」拆成可逆步骤,每步成功后写日志,失败时按日志反向执行 Cancel
- 绝不把
http.Post()放在tx.Transaction()闭包里——它既不参与事务,又可能因超时阻塞整个事务











