769 字
4 分钟
事务与并发控制
GORM 事务与并发控制
闭包事务
推荐使用闭包形式,让 GORM 负责提交和回滚:
err := db.Transaction(func(tx *gorm.DB) error { if err := tx.Create(&order).Error; err != nil { return err // 自动回滚 }
if err := tx.Model(&Account{}). Where("id = ? AND balance >= ?", accountID, amount). Update("balance", gorm.Expr("balance - ?", amount)).Error; err != nil { return err }
return nil // 自动提交})事务内的所有数据库操作都必须使用 tx,不能又使用外层 db,否则操作可能跑到事务之外。
手动事务
需要更细的控制时:
tx := db.Begin()if tx.Error != nil { return tx.Error}
defer func() { if r := recover(); r != nil { tx.Rollback() panic(r) }}()
if err := tx.Create(&order).Error; err != nil { tx.Rollback() return err}if err := tx.Commit().Error; err != nil { return err}return nil手动模式必须覆盖 Begin、每个操作、Commit、Rollback 和 panic,否则容易留下未完成事务。
上下文与超时
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)defer cancel()
err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return tx.Create(&record).Error})请求取消时,驱动是否立即停止 SQL 取决于驱动和数据库支持,但应用层应该把请求上下文传到底层。
默认事务与性能
GORM 默认会为写操作使用事务以保证数据一致性。SkipDefaultTransaction 可以降低单条写入开销,但只适合单条写入本身具备足够原子性、且业务不依赖默认事务的场景。
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ SkipDefaultTransaction: true,})关闭默认事务不会关闭显式 db.Transaction(...),关键业务仍然可以自己开启事务。
嵌套事务与保存点
err := db.Transaction(func(tx *gorm.DB) error { if err := tx.Create(&order).Error; err != nil { return err }
return tx.Transaction(func(nested *gorm.DB) error { if err := nested.Create(&audit).Error; err != nil { return err // 通常回滚到保存点,不一定回滚外层事务 } return nil })})嵌套事务的具体行为依赖数据库对保存点的支持;不要把它当成跨数据库的完全一致抽象。
行锁
读取后要基于当前值更新时,可以使用数据库行锁:
var account Accounterr := db.Transaction(func(tx *gorm.DB) error { if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("id = ?", accountID). First(&account).Error; err != nil { return err }
if account.Balance < amount { return errors.New("insufficient balance") } account.Balance -= amount return tx.Save(&account).Error})锁的范围、等待和死锁行为由数据库隔离级别与执行计划决定;事务应尽量短,多个事务获取锁时保持一致顺序。
乐观并发控制
可以使用版本号或带旧值条件的更新:
result := db.Model(&Document{}). Where("id = ? AND version = ?", id, oldVersion). Updates(map[string]any{ "content": newContent, "version": gorm.Expr("version + 1"), })if result.Error != nil { return result.Error}if result.RowsAffected == 0 { return errors.New("document was changed by another request")}事务原则
- 事务边界放在业务用例层,而不是每个 Repository 方法内部各开一个事务。
- 事务中不要执行长时间网络请求、文件 IO 或等待用户操作。
- 事务中所有读写使用同一个
tx,并传递同一个上下文。 - 对死锁、超时和唯一键冲突设计可观测日志及有限重试策略。
- 不要把事务对象跨 goroutine 共享。
官方资料
分享文章
生成精美分享图或复制链接,与更多人分享本文。
继续阅读
换条路线
从其他文章中稳定抽取
最后更新于 ,距今已过 77 天
部分内容可能已过时