必须传gorm.DB事务实例,不能传原始gorm.DB连接对象;否则事务隔离失效,操作绕过事务直接落库,导致部分提交、脏读、超卖及连接池耗尽等问题。

必须传 *gorm.DB(即事务实例),不能传原始 *gorm.DB 连接对象;否则事务隔离失效,数据可能写入主连接,导致部分提交、脏读或连接池耗尽。
为什么不能把原始 db 传进 Service?
原始 db 是无状态连接池句柄,每次调用 Create、Update 等方法都会从池中取新连接。即使你在 Handler 层开了事务,Service 里若用的是原始 db,所有操作都绕过事务,直接落库——等于“表面在事务里,实际在裸跑”。
常见错误现象:
- 事务内扣库存成功,但记日志失败后整体回滚,日志却已写入数据库
- 并发下单时出现超卖,查库存是事务内查的,但扣减用了原始
db - PostgreSQL 报错
ERROR: current transaction is aborted,因为混用了连接上下文
Service 函数签名必须显式接收 *gorm.DB
Service 层不持有 DB 实例,只接收上游传入的事务对象。这是分层清晰、可测、可复用的前提。
正确示例:
func (s *OrderService) CreateOrder(tx *gorm.DB, userID uint, items []Item) error {
// 所有 db 操作都用 tx
if err := tx.Create(&order).Error; err != nil {
return err
}
if err := tx.Model(&order).Association("Items").Append(items); err != nil {
return err
}
return nil
}
错误示例(隐藏陷阱):
设置基于 systemd 的看门狗,使 Node.js 长运行服务(如 Telegram 机器人、Express 仪表盘等)在 shell 退出、SSH 断开等情况下保持运行。
// ❌ Service 内部 new 一个 db 或从全局变量取 db
func (s *OrderService) CreateOrder(userID uint, items []Item) error {
db := s.db // ← 全局 db 或注入的 *gorm.DB,不是事务!
return db.Create(&order).Error
}
关键点:
- Handler 或 Controller 层负责开启事务并传入
tx - Service 不该知道“当前有没有事务”,它只保证:给它
tx就用tx,给它db就用db(但生产环境永远只传tx) - Repository 层同理,接口方法参数也应为
*gorm.DB,而非隐式依赖
db.Transaction() 足够用,别提前 Begin()
90% 场景下,你不需要手动 Begin()。直接用 db.Transaction() 更安全:
err := db.Transaction(func(tx *gorm.DB) error {
return orderService.CreateOrder(tx, userID, items)
})
它自动处理:
- panic 时回滚
- 返回非 nil error 时回滚
- 成功时自动
Commit() - 不会因忘记
Commit()导致连接卡死
手动 Begin() 只适合两种情况:
- 需要跨多个函数显式传递事务对象(比如 Service → Repository 分层明确,且每个 repo 方法都要接收
tx) - 要用
SavePoint做局部回滚(例如校验失败只撤回某一步,不影响外层事务)
最易被忽略的一点:事务对象是**有状态的**,一旦 RollbackTo() 或 Commit() 后,该 tx 实例不可再用;继续调用会 panic 或静默失败。务必确保每个函数只用一次传入的 tx,不要缓存、不要复用、不要试图“重置”。










