kratos中事务管理需手动控制,wire注入*sql.tx获取函数,service层显式开启、提交或回滚;错误码生成不参与事务,rollback必须显式调用且不可依赖中间件;ent/gorm事务行为不同,测试须用真实db并注意隔离级别。

kratos 用 wire 注入事务管理器时,必须显式传递 *sql.Tx
kratos 本身不封装事务逻辑,它依赖底层 SQL 驱动(如 database/sql)和你自己的业务封装。常见错误是直接在 service 层 new 一个 *sql.DB 就调用 Query/Exec,这样每次都是独立事务,根本无法回滚。
正确做法是:在 wire provider 中注入一个能返回 *sql.Tx 的函数,比如:
func NewTx(db *sql.DB) func() (*sql.Tx, error) {
return func() (*sql.Tx, error) {
return db.Begin()
}
}
然后在 service 方法中显式开启、提交或回滚:
- 调用该函数获取
*sql.Tx - 所有数据库操作改用
tx.Query、tx.Exec等 - 成功则调用
tx.Commit(),失败则tx.Rollback() - 注意:即使
Commit()失败,也应再调用一次Rollback()(MySQL 的COMMIT可能因锁等待超时而失败)
proto 定义错误码后,go generate 生成的 error 函数不参与事务控制
kratos 的 protoc-gen-go-errors 插件只负责把枚举转成强类型 error 变量(如 errors.BadRequest),它和事务生命周期完全无关。很多人误以为抛出这个 error 就会自动回滚——不会。
事务回滚必须由你手动触发,例如:
if err := userRepo.Create(tx, u); err != nil {
tx.Rollback()
return errors.InternalServer(err)
}
关键点:
- error 类型只是语义标识,不影响 DB 状态
- rollback 必须在
defer之外显式写,否则 panic 捕获不到时事务就悬着 - 别依赖中间件自动 rollback:kratos 默认无事务中间件,需自行封装或引入第三方(如
ent的ent.Tx)
用 ent 或 gorm 替代原生 database/sql 时,事务行为差异大
kratos 不绑定 ORM,但团队常选 ent 或 gorm。这两者对事务的封装程度不同,直接影响 rollback 写法:
- ent:推荐用
ent.Tx,它包装了*sql.Tx,tx.Create().Exec(ctx)失败时不会自动 rollback,仍需手动tx.Rollback() - gorm:支持
Session(&gorm.Session{NewDB: true})创建新事务,但要注意db.Transaction()是闭包式 API,内部 panic 会自动 rollback,而显式tx.Rollback()需确保只调一次 - 性能影响:ent 的 Tx 是轻量 wrapper,开销小;gorm 的
Transaction()会新建 session,频繁调用可能增加 GC 压力
测试事务回滚必须用真实 DB,不能只 mock *sql.Tx
单元测试里 mock *sql.Tx 的 Commit/Rollback 方法,容易掩盖真实问题。比如 MySQL 在 ROLLBACK 时若连接已断,会报 io: read/write on closed pipe,但 mock 根本不会触发这类错误。
建议:
- 用
testcontainers-go启一个临时 MySQL 容器跑集成测试 - 在 test setup 中执行
TRUNCATE TABLE清空数据,避免事务残留干扰 - 验证点不只是“有没有调用 Rollback”,还要查 DB 表是否真没写入(尤其涉及外键或触发器时)
最容易被忽略的是:事务隔离级别默认是 REPEATABLE READ,在测试中并发读写可能因 MVCC 快照导致“看似回滚成功,实则其他事务已读到脏数据”。需要在测试连接串里显式加 ?isolation=READ-COMMITTED 控制。











