Skip to content
Go
事务与锁

事务与锁

GORM 提供了灵活的事务支持:自动事务(每个写操作为一个事务)、手动事务、SavePoint 回退、嵌套事务。同时支持悲观锁和乐观锁。


自动事务(默认)

GORM 默认将单个 CreateUpdateDelete 操作包裹在事务中执行:

// 下面这行代码实际上执行了:
// BEGIN
// INSERT INTO users (name, age) VALUES ('张三', 18)
// COMMIT
db.Create(&User{Name: "张三", Age: 18})
// 若要关闭默认事务(提升写性能约 30%)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
    SkipDefaultTransaction: true,
})

性能提示:关闭默认事务可提升大量单条写入的性能(约 30%),但会失去单条写入的原子性。需要保证单个 Create 的原子性时,开启默认事务。


手动事务

基础事务

// 方式一:闭包事务(推荐 — 自动 Commit/Rollback)
err := db.Transaction(func(tx *gorm.DB) error {
    // 所有在 tx 上的操作都在一个事务内
    if err := tx.Create(&User{Name: "张三"}).Error; err != nil {
        return err // 返回错误 → 自动 Rollback
    }

    if err := tx.Create(&Order{UserID: 1, Amount: 100}).Error; err != nil {
        return err
    }

    return nil // 返回 nil → 自动 Commit
})
// 方式二:手动控制(需要手动管理 Commit/Rollback)
tx := db.Begin()
defer func() {
    if r := recover(); r != nil {
        tx.Rollback()
    }
}()

if err := tx.Create(&User{Name: "张三"}).Error; err != nil {
    tx.Rollback()
    return
}

if err := tx.Create(&Order{UserID: 1, Amount: 100}).Error; err != nil {
    tx.Rollback()
    return
}

tx.Commit()

💡 最佳实践:优先使用闭包事务 db.Transaction()——它会自动处理 Commit/Rollback 和 panic 恢复,避免遗漏。

SavePoint — 事务内回退点

err := db.Transaction(func(tx *gorm.DB) error {
    // 创建用户
    tx.Create(&User{Name: "张三"})

    // 设置 SavePoint
    tx.SavePoint("sp1")

    // 尝试某操作
    if err := tx.Create(&Order{UserID: 1, Amount: -100}).Error; err != nil {
        // 回退到 sp1(撤销负金额订单的创建)
        tx.RollbackTo("sp1")
    }

    // 继续其他操作
    return tx.Create(&Order{UserID: 1, Amount: 200}).Error
})

SavePoint 适用于:一个事务内需要尝试多个操作,且部分操作失败不应影响整体事务。


嵌套事务

GORM 支持嵌套事务——内部事务的 Rollback 不影响外部事务:

db.Transaction(func(tx *gorm.DB) error {
    tx.Create(&User{Name: "张三"}) // 外部事务

    // 嵌套事务
    tx.Transaction(func(tx2 *gorm.DB) error {
        tx2.Create(&Order{UserID: 1, Amount: 100})
        return errors.New("模拟失败") // 仅回滚嵌套事务
    })

    // 外部事务继续执行,不受嵌套事务失败影响
    tx.Create(&User{Name: "李四"})
    return nil
})
// 最终结果:张三创建成功,李四创建成功,订单未创建

🔬 深入原理:MySQL 不支持真正的嵌套事务。GORM 通过 SAVEPOINT 模拟:

嵌套事务 BEGIN → SAVEPOINT sp_xxx
嵌套事务 COMMIT → RELEASE SAVEPOINT sp_xxx
嵌套事务 ROLLBACK → ROLLBACK TO SAVEPOINT sp_xxx

悲观锁(Pessimistic Lock)

悲观锁在读取时锁定行,直到事务提交才释放:

// MySQL / PostgreSQL: SELECT ... FOR UPDATE
db.Transaction(func(tx *gorm.DB) error {
    var product Product

    // 锁定行,阻止其他事务修改
    tx.Clauses(clause.Locking{Strength: "UPDATE"}).
        Where("id = ?", 1).
        First(&product)

    if product.Stock <= 0 {
        return errors.New("库存不足")
    }

    // 扣库存
    return tx.Model(&product).
        Where("id = ?", product.ID).
        Update("stock", gorm.Expr("stock - ?", 1)).Error
})
// 生成 SQL:SELECT * FROM products WHERE id = 1 FOR UPDATE

悲观锁类型

Strength MySQL PostgreSQL
"UPDATE" FOR UPDATE(排他锁) FOR UPDATE
"SHARE" FOR SHARE / LOCK IN SHARE MODE(共享锁) FOR SHARE
"NO KEY UPDATE" FOR NO KEY UPDATE
"KEY SHARE" FOR KEY SHARE
// 共享锁(允许其他事务读,但阻止写)
tx.Clauses(clause.Locking{Strength: "SHARE"}).First(&product)

// 跳过已锁定的行(避免等待)
tx.Clauses(clause.Locking{Strength: "UPDATE", Options: "NOWAIT"}).First(&product)
// MySQL 8.0+: SELECT ... FOR UPDATE NOWAIT
// PostgreSQL: SELECT ... FOR UPDATE NOWAIT

// 跳过已锁定的行(MySQL)
tx.Clauses(clause.Locking{Strength: "UPDATE", Options: "SKIP LOCKED"}).First(&product)

💡 扣库存场景的标准模式:

  1. BEGIN
  2. SELECT ... FOR UPDATE(锁定目标行)
  3. 校验库存是否充足
  4. UPDATE stock = stock - 1
  5. COMMIT

乐观锁(Optimistic Locking)

乐观锁通过版本号字段实现,不使用数据库行锁:

GORM 内置乐观锁

import "gorm.io/plugin/optimisticlock"

type Product struct {
    gorm.Model
    Name  string
    Stock int
    Version optimisticlock.Version // 自动版本号管理
}

// 更新时自动携带 version 条件
db.Save(&product)
// UPDATE products SET stock = 9, version = version + 1
// WHERE id = 1 AND version = 2

// 如果 version 已被其他事务修改 → RowsAffected = 0

手动实现乐观锁

type Product struct {
    gorm.Model
    Stock   int
    Version int
}

// 更新时检查版本号
result := db.Model(&Product{}).
    Where("id = ? AND version = ?", product.ID, product.Version).
    Updates(map[string]interface{}{
        "stock":   newStock,
        "version": gorm.Expr("version + 1"),
    })

if result.RowsAffected == 0 {
    return errors.New("并发冲突:数据已被他人修改,请重试")
}

悲观锁 vs 乐观锁对比

维度 悲观锁 乐观锁
机制 数据库行锁 (FOR UPDATE) 版本号比对
并发冲突频率 适合高冲突场景 适合低冲突场景
性能 等待锁,吞吐量较低 无锁等待,吞吐量高
实现复杂度 低(数据库原生支持) 需重试逻辑
死锁风险 有(需注意锁顺序)
适用场景 扣库存、转账 编辑表单、更新配置

💡 扣库存等涉及有限资源竞争的场景优先用悲观锁;编辑表单等用户主动操作场景优先用乐观锁


死锁预防

// 错误的锁顺序(可能死锁):
// 事务A: 锁产品1 → 锁产品2
// 事务B: 锁产品2 → 锁产品1

// 正确做法:始终按相同顺序加锁
func TransferStock(tx *gorm.DB, fromID, toID uint, quantity int) error {
    // 始终先锁 ID 较小的记录
    first, second := fromID, toID
    if fromID > toID {
        first, second = toID, fromID
    }

    var p1, p2 Product
    tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&p1, first)
    tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&p2, second)

    // ... 执行转移逻辑
    return nil
}

💡 死锁预防的核心原则:所有事务对资源的锁定顺序必须一致。


事务隔离级别

// MySQL 查看/设置隔离级别
db.Exec("SET TRANSACTION ISOLATION LEVEL READ COMMITTED")

// 或通过 DSN 配置
// MySQL: ...&transaction_isolation=READ-COMMITTED
// PostgreSQL: ...&default_transaction_isolation=read committed
隔离级别 脏读 不可重复读 幻读 MySQL 默认 PostgreSQL 默认
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ ✅(部分)
SERIALIZABLE

💡 PostgreSQL 默认 READ COMMITTED;MySQL 默认 REPEATABLE READ。大多数 Web 应用使用 READ COMMITTED 即可满足需求。


常见陷阱汇总

陷阱 说明
🚨 事务忘记 Commit 手动事务忘记 tx.Commit(),连接一直持有锁
🚨 长事务 事务内做网络请求/文件 IO → 锁持有时间过长
🚨 事务未重试 死锁检测后不重试,直接返回错误给用户
🚨 默认事务开销 单条写也 BEGIN→COMMIT,频繁写入有性能开销
🚨 锁升级 MySQL 中不合理的查询条件导致行锁升级为表锁
🚨 非索引列锁 MySQL 中 FOR UPDATE 通过非索引列过滤可能锁定全表
🚨 PostgreSQL 锁差异 PG 的 FOR UPDATE 与 MySQL 行为不同,注意测试