必须用db.transaction封装单库事务,因它自动处理commit/rollback/panic;跨服务无自动回滚,只能靠saga/tcc/消息队列实现最终一致性;混用db与tx会导致操作脱离事务,破坏原子性。

单数据库事务必须用 db.Transaction 封装,跨服务事务没有“自动回滚”这回事,只能靠 Saga、TCC 或消息队列做最终一致性。
为什么不能在事务里直接调用其他服务的 DB 方法?
事务对象(比如 *gorm.DB)是会话隔离的。你在 tx := db.Begin() 后拿到的 tx,只对它执行的 SQL 生效;如果中间调用了另一个 service 的函数,而那个函数用的是原始 db,那它的写操作就落在主连接池里,完全不参与当前事务。
常见错误写法:
tx := db.Begin() defer tx.Rollback() // ❌ 错误:没判断是否已 Commit,也没 recover panic userRepo.Create(tx, u) // ✅ 正确:传入 tx orderService.CreateOrder(u) // ❌ 危险:内部用的是 db,不是 tx
- 所有数据库操作必须显式传入事务对象,不能隐式依赖全局
db - 跨 service 调用必须通过 API(HTTP/gRPC),不能共享 DB 实例
- 若真要复用逻辑,应把数据访问层抽象为接受
*gorm.DB的函数,而非硬编码db
嵌套事务其实是 SavePoint,不是真正嵌套
GORM 的 tx.Transaction() 不会开启新事务,只是建一个保存点(SavePoint)。它共享底层连接,ROLLBACK TO sp1 只能回滚到该点,外层仍需 Commit() 或 Rollback()。
容易踩的坑:
- 重复使用相同保存点名,后一次
SavePoint("sp")会覆盖前一次,导致中间状态不可回滚 - 在保存点内执行
CREATE TABLE等 DDL,在 MySQL 中会隐式提交,整个事务原子性被破坏 - 误以为子事务失败会自动触发外层回滚——实际不会,必须手动检查返回错误并决定是否终止外层
跨服务事务必须放弃 ACID,转向最终一致性
没有数据库能跨 PostgreSQL + MySQL + Redis 做原子提交。Golang 微服务里,你得选一种可落地的补偿模型:
- Saga 模式:适合流程长、允许短时异步的场景(如下单→扣库存→支付)。每步本地事务成功后发事件,失败则按反向顺序调用 Cancel 接口
-
TCC 模式:适合强一致要求高、资源可控的场景(如转账)。每个服务提供
Try/Confirm/Cancel三接口,协调器统一调度 -
本地消息表 + MQ:支付成功后,先在本地库写一条“待发送支付成功消息”,再由定时任务投递到 Kafka;下游消费时必须幂等(比如用
order_id做唯一索引)
无论哪种,都得配 context.WithTimeout 防卡死,加重试退避,记录关键步骤日志用于人工干预。
最常被忽略的一点:事务中不要做任何非 DB 操作——发邮件、写文件、调外部 HTTP 接口。这些行为无法回滚,一旦 DB 回滚而通知已发出,就会造成状态错乱。把它们移出事务体,改用事件驱动或异步任务队列。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











