事务必须显式开启和结束,不能依赖defer自动回滚;db.Transaction自动处理Commit/Rollback/panic捕获,推荐作为默认方案;手动Begin仅用于SavePoint等底层场景,须严格配对Commit或Rollback并recover。

事务必须显式开启和结束,不能依赖 defer 自动回滚
Go 没有“自动事务上下文”概念,db.Transaction 返回的新 *gorm.DB 实例是独立会话,defer 在函数退出时执行,但此时事务可能早已提交或已 panic,导致回滚失效。常见错误是写成:
tx := db.Begin() defer tx.Rollback() // ❌ 错误:没判断是否已 Commit,也没处理 panic
正确做法是用 defer + recover 组合,或更推荐——直接用 db.Transaction 函数封装:
- 始终用
db.Transaction(func(tx *gorm.DB) error { ... }),它内部自动处理 Commit / Rollback / panic 捕获 - 若需手动控制(如嵌套事务、保存点),才用
Begin(),且必须配对Commit()或Rollback(),并在 panic 后显式recover() - 不要在事务内调用其他未传入
tx的 DB 方法,否则操作落在默认连接池,不参与当前事务
嵌套事务实际是保存点(SavePoint),不是真正嵌套
GORM 不支持传统意义上的嵌套事务(即子事务可独立提交),而是通过 SavePoint 模拟。调用 tx.SavePoint("sp1") 后,后续失败可回滚到该点,但整个外层事务仍需最终 Commit() 或 Rollback()。
容易踩的坑:
立即学习“go语言免费学习笔记(深入)”;
- 误以为
tx.Transaction(...)会开启新事务——其实只是新建一个带保存点的子作用域,共享同一底层连接 - 保存点名重复导致
SavePoint覆盖,回滚时跳过中间状态 - 在保存点内执行 DDL(如
CREATE TABLE)可能被某些数据库(如 MySQL)隐式提交,破坏事务原子性
事务中并发读写易触发死锁,需按固定顺序加锁
当多个 goroutine 并发更新同一组记录(如订单+库存),且加锁顺序不一致时,MySQL / PostgreSQL 会检测到循环等待并主动 kill 其中一个事务,报错 ERROR: deadlock detected(PostgreSQL)或 ErrDeadlock(MySQL)。
解决方式不是重试就行,关键是预防:
- 对涉及多表更新的操作,约定行锁顺序:比如总是先锁
orders表再锁inventory表,避免 A 先锁 order 后锁 inventory,B 反过来 - 用
SELECT ... FOR UPDATE显式加锁时,确保 WHERE 条件能命中索引,否则会锁整张表 - 事务粒度要小:长事务增加锁持有时间,提高冲突概率;把非关键逻辑(如日志写入、通知发送)移出事务体
跨服务事务无法靠 GORM 解决,得用 Saga 或消息补偿
db.Transaction 只管单个数据库实例。如果业务要同时扣库存、生成支付单、发优惠券,而三者分别在不同数据库甚至不同服务中,GORM 无能为力。
这时候硬上分布式事务(如两阶段提交)成本高、性能差、可用性低。务实做法是:
- 接受最终一致性,用本地消息表 + 消息队列实现可靠异步通知
- 核心链路走 Saga 模式:每步都配对补偿操作(如“扣库存”对应“补库存”),失败时反向执行补偿
- 绝不把 HTTP 调用塞进 GORM 事务里——网络延迟、超时、重试都会让事务挂起数十秒,拖垮整个连接池
事务边界止于数据库连接,再往外就是业务协议的事。想靠 ORM 解决跨库问题,只会让代码越来越难 debug。


















