中间件不能直接开启提交事务,因不持有数据库连接且事务对象无法传递至handler;事务必须由handler显式控制,或通过context注入tx并手动管理。

中间件里不能直接开启和提交事务
因为 Iris 中间件本身不持有数据库连接,ctx 也不带 *gorm.DB 或 *sql.Tx 实例。你在中间件里调 db.Begin() 得到的 *sql.Tx 如果没传给后续 handler,就只能在中间件作用域内用——而 handler 一执行就脱离了这个作用域,事务对象被丢弃,根本没法 commit 或 rollback。
常见错误现象是:中间件里 tx, _ := db.Begin(),然后 ctx.Next(),handler 里用 db.Create() —— 这个操作走的仍是原 *gorm.DB 连接池,跟 tx 完全无关,事务形同虚设。
- 事务必须由 handler 自己控制,或通过依赖注入把
*gorm.DB包装成带事务能力的封装(比如BeginTx(ctx)) - 中间件唯一能做的,是统一注入一个“事务上下文”(如
ctx.Values().Set("tx", tx)),但 handler 必须主动取、主动用、主动结束 - 别在中间件里
defer tx.Commit()——ctx.Next()后 handler panic 了,defer不会触发(Iris 的 recover 中间件捕获 panic 后,函数已退出)
用 context.Value + defer 管理单请求事务(推荐轻量场景)
如果你用的是 database/sql,可以在 handler 开头手动开事务,用 defer 保证收尾:
func userUpdateHandler(ctx iris.Context) {
tx, err := writeDB.Begin()
if err != nil {
ctx.StatusCode(500)
ctx.WriteString("tx begin failed")
return
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
panic(r)
}
}()
// 执行 SQL
_, err = tx.Exec("UPDATE users SET name=? WHERE id=?", ctx.Params().Get("name"), ctx.Params().Get("id"))
if err != nil {
tx.Rollback()
ctx.StatusCode(500)
return
}
tx.Commit()
}
注意:writeDB 是你初始化好的主库 *sql.DB,不能混用 readDB;事务期间所有查询也必须走 tx,否则读不到未提交数据(脏读可控,但一致性崩了)。
GORM v2 的 Session + Transaction 配合方式
GORM v2 提供了更自然的事务封装,但依然不能靠中间件自动 wrap。正确做法是 handler 内显式调用 db.Session(&gorm.Session{NewDB: true}).Begin(),或用 db.Transaction() 函数式写法:
-
db.Transaction(func(tx *gorm.DB) error { ... })会自动 commit/rollback,适合逻辑较短、无复杂分支的场景 - 需要跨多个 handler 步骤(比如校验 → 更新 → 发通知)时,得自己 hold 住
*gorm.DB实例并传参,或用ctx.Values().Set("gorm_tx", tx)注入 - 若用了主从分离(
ReplicaDB),事务内所有操作强制走主库,GORM 会自动忽略Replicas配置 —— 这点不用额外处理,但得心里有数
错误示例:db.WithContext(ctx).First(&u) 在事务中仍可能走从库(取决于 GORM 版本和配置),必须确保 db 是事务实例本身,不是原始 *gorm.DB。
为什么不要用中间件统一 StartTransaction / Commit
看似省事,实则埋雷:
- 不是所有请求都需要事务(比如纯 GET 查询),中间件一概开启浪费连接和锁资源
- 事务生命周期难对齐:中间件
Use()是前置,但 handler 可能提前return,Done()后置又无法感知是否该 commit - 嵌套事务行为不可控:如果 handler 内部又调
tx.Begin(),不同驱动表现不一(MySQL 不支持真嵌套,PostgreSQL 支持 savepoint) - 分布式事务?别想 —— Iris + 单机 GORM 无法解决跨库、跨服务的一致性,这时候该上消息队列或 Saga 模式
真正容易被忽略的一点:事务超时。GORM 默认不限制,MySQL 服务端 wait_timeout 通常 8 小时,但长事务会阻塞 MVCC 清理、撑爆 undo log。务必在 db.Transaction() 外包一层 context.WithTimeout(),并在 handler 开头就设定好。











