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 Account
err := 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 共享。

官方资料#

事务与并发控制
https://shirone.mysqil.com/posts/gorm/事务与并发控制/
作者
HikariTish
发布于
2026-07-30
许可协议
CC BY-NC-SA 4.0

分享文章

生成精美分享图或复制链接,与更多人分享本文。

继续阅读

沿着主题读

基于共同的标签与分类

换条路线

从其他文章中稳定抽取