必须显式打日志记录事务生命周期。GORM 不自动记录 Begin/Commit/Rollback,需在事务工厂中生成唯一 tx_id 并透传 context,SQL 和事务操作日志均需携带 tx_id 与 req_id,禁用不可靠钩子,统一由业务层控制日志埋点。

事务开始和提交/回滚必须显式打日志
GORM 本身不自动记录事务生命周期事件,Begin、Commit、Rollback 这些动作不会出现在默认日志里。你看到的只是单条 SQL,无法判断它们是否在同一个事务中执行。想审计事务行为,得自己在调用 db.Begin()、tx.Commit()、tx.Rollback() 前后手动写日志。
常见错误是只在 DAO 层打 SQL 日志,结果线上查问题时发现“用户余额没扣但订单创建了”,却没法确认这两条操作是否进了同一事务——因为日志里根本没标事务 ID 或起止标记。
- 每次
db.Begin()时生成唯一tx_id(如uuid.New().String()),并存入 context:ctx = context.WithValue(ctx, "tx_id", txID) - 把
tx_id注入到后续所有tx.WithContext(ctx)调用中,确保每条 SQL 日志都带该字段 -
tx.Commit()和tx.Rollback()必须单独打日志,且 level 设为Warn(回滚)或Info(成功提交),不能混在 SQL 日志里 - 避免用
time.Now()记事务耗时,应从Begin到Commit/Rollback手动计时,因为 GORM 的Trace只对单条 SQL 生效
别依赖 GORM 钩子捕获事务边界
AfterBegin、BeforeCommit 这类钩子看似可用,但实际不可靠:它们只在模型层触发,绕过 ORM 的原生 SQL(如 tx.Exec("UPDATE ..."))或批量操作(tx.CreateInBatches)就完全不走钩子;更严重的是,BeforeCommit 在事务真正提交前执行,此时若发生死锁或超时,最终仍会回滚,但日志已写了“即将提交”,造成误导。
正确做法是把事务日志控制权收回到业务层,即所有 db.Begin() 都由统一的事务工厂函数封装,日志也在这里统一埋点。
- 钩子内拿不到
tx.RowsAffected或真实错误(比如唯一约束冲突发生在 Commit 阶段,BeforeCommit看不到) - 多个嵌套事务(SavePoint)时,钩子无法区分层级,
tx.SavePoint("sp1")不触发任何钩子 - 如果用了
Config.SkipHooks: true(常见于性能敏感路径),钩子直接失效
SQL 日志里必须标注是否在事务中
默认 GORM 日志不体现事务上下文,SELECT 和 UPDATE 看起来完全独立。但审计时你需要一眼识别“这条 UPDATE 是在事务里执行的”,否则无法还原数据一致性逻辑。
最轻量的办法是在自定义 logger.Writer 的 Printf 中检查 stmt.DB.Statement.ConnPool != nil,但这只能粗略判断连接是否复用;更准的方式是透传 tx_id 并在日志模板里显式拼接。
- 不要用
db.Statement.Context.Value("tx_id")判断——普通db实例没有这个 key,容易 panic - 推荐在事务工厂里把
tx_id写进tx.Statement.Context,然后在Trace方法中取:tx.Statement.Context.Value("tx_id") - 日志格式建议:
[tx_id=abc123] UPDATE users SET balance = ? WHERE id = ?,而非仅加个 flag 字段 - 注意
PrepareStmt: true下,预编译语句的日志可能不带tx_id,需在conn.PrepareContext阶段也透传 context
事务日志要和请求 ID 对齐,但不能强依赖 Gin Context
HTTP 请求 ID(如 X-Request-ID)和事务 ID 必须能关联,否则审计时无法把“下单失败”对应到具体哪次数据库操作。但千万别在事务日志里直接读 gin.Context,因为事务可能由定时任务、消息消费等非 HTTP 场景发起。
可靠的做法是统一用 context.Context 作为载体,在入口处注入 req_id 和 tx_id,再逐层透传。Gin 中间件里做的只是其中一种注入方式,不是唯一来源。
- 事务工厂函数应接受
ctx context.Context参数,而不是假设它来自 HTTP - 日志字段必须同时包含
req_id(可为空)和tx_id(必有),避免漏掉后台任务场景 - 异步写事务日志时,务必用
ctx而非闭包捕获的局部变量,防止 goroutine 启动晚导致ctx已 cancel - 别在
Rollback日志里打印 error 全量堆栈——只留err.Error(),避免日志刷屏且泄露内部路径


















