kratos 不支持 yaml 配置事务,因事务是运行时资源,需通过依赖注入提供事务工厂并在业务逻辑中显式调用 begin/commit/rollback;config.yaml 仅加载静态配置,无法参与事务生命周期管理。

Kratos 本身不提供自动事务管理,必须手动控制 *sql.Tx 或 ORM 的事务对象;配置事务不是改 YAML,而是设计数据层接口、注入事务工厂、并在业务逻辑中显式调用 Begin/Commit/Rollback。
为什么不能在 config.yaml 里配事务
Kratos 的 config 模块只负责加载配置项(如数据库地址、超时时间),它不参与运行时事务生命周期。事务是运行时资源,需要由 *sql.DB 创建 *sql.Tx,或由 *gorm.DB 调用 Transaction() 生成上下文绑定的事务实例。把“开启事务”写进配置文件没有意义——它既不会自动注入到 handler,也无法跨函数传递。
常见错误现象:
- 在 service 层直接 new 一个 *sql.DB 调用 Exec,结果每次都是独立短连接,根本不在同一事务内
- 以为在 conf.Data 结构体里加个 EnableTx bool 字段就能开关事务——这个字段对 DB 行为零影响
Wire 中如何注入事务能力
必须提供一个能返回 *sql.Tx(或 *gorm.DB)的函数,供上层按需获取。这不是“配置”,而是依赖注入的契约设计。
使用原生 database/sql 时,典型 wire provider 写法:
func NewTx(db *sql.DB) func() (*sql.Tx, error) {
return func() (*sql.Tx, error) {
return db.Begin()
}
}
然后在 service 方法中使用:
- 调用该函数拿到
tx, err := txFactory() - 所有 repo 操作改用
tx.Query/tx.Exec/tx.Prepare - 成功后调
tx.Commit(),失败立即tx.Rollback() -
Commit()也可能失败(比如 MySQL 锁等待超时),此时仍应再调一次Rollback()防止连接泄露
GORM 场景下怎么统一事务入口
若用 GORM,推荐在 data 层抽象出 ExecTx 方法,让 usecase 控制事务边界,避免 data 层每个方法都重复判断是否在事务中。
关键点:
- 定义接口:
type Transaction interface { ExecTx(context.Context, func(ctx context.Context) error) error } - 实现时用
db.WithContext(ctx).Transaction(...),内部闭包中把*gorm.DB注入 ctx(如存入context.WithValue(ctx, txKey{}, tx)) - 各 repo 方法通过
DB(ctx)获取当前 DB 实例:有事务就返回tx,否则返回原始db - 这样同一个 repo 方法既能 standalone 调用,也能被包裹在事务中复用
最容易被忽略的三个细节
事务不是“开了就完事”。这三个点不处理好,线上大概率出现数据不一致或连接泄漏:
-
Rollback()必须显式写在 error 分支里,不能依赖 defer——如果中间 panic 且没被 recover,defer 不会执行 - proto 生成的
errors.BadRequest等只是 error 类型,和事务状态完全无关;抛出它不会触发回滚 - 测试事务逻辑时,必须用真实数据库 +
ReadCommitted或更高隔离级别,内存 SQLite 或 mock 不会暴露锁竞争、脏读等问题











