事务基础

事务是数据库操作的原子单位。要么全部成功,要么全部失败,没有中间状态。银行转账是经典例子:A 账户扣款,B 账户入账,两步操作必须同时成功或同时失败。

为什么需要事务

没有事务时:

func Transfer(fromID, toID uint, amount float64) error {
    db.Model(&Account{}).Where("id = ?", fromID).Update("balance", gorm.Expr("balance - ?", amount))
    db.Model(&Account{}).Where("id = ?", toID).Update("balance", gorm.Expr("balance + ?", amount))
    return nil
}

如果第一步成功、第二步失败,钱就凭空消失了。事务保证这种情况不会发生。

手动事务

最基本的事务用法:

tx := db.Begin()

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

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

tx.Commit()
return nil

Begin() 开始事务,Commit() 提交,Rollback() 回滚。

事务闭包

更简洁的写法,自动处理提交和回滚:

err := db.Transaction(func(tx *gorm.DB) error {
    if err := tx.Create(&User{Name: "张三"}).Error; err != nil {
        return err
    }
    if err := tx.Create(&Order{Amount: 100}).Error; err != nil {
        return err
    }
    return nil
})

if err != nil {
    fmt.Println("事务失败:", err)
}

闭包返回 nil 自动提交,返回错误自动回滚。

事务隔离级别

GORM 支持设置事务隔离级别:

db.Begin(&sql.TxOptions{
    Isolation: sql.LevelReadCommitted,
})

常用隔离级别:

级别说明
LevelDefault数据库默认
LevelReadUncommitted读未提交
LevelReadCommitted读已提交
LevelRepeatableRead可重复读
LevelSerializable串行化

MySQL 默认是 REPEATABLE READ,PostgreSQL 默认是 READ COMMITTED

只读事务

db.Begin(&sql.TxOptions{
    ReadOnly: true,
})

只读事务只能执行查询操作,尝试写入会报错。适合报表统计等场景。

事务中的错误处理

tx := db.Begin()
defer func() {
    if r := recover(); r != nil {
        tx.Rollback()
    }
}()

if err := tx.Create(&user).Error; err != nil {
    tx.Rollback()
    return err
}

if err := tx.Create(&order).Error; err != nil {
    tx.Rollback()
    return err
}

return tx.Commit().Error

用 defer + recover 捕获 panic,确保事务回滚。

实际案例

银行转账

func Transfer(db *gorm.DB, fromID, toID uint, amount float64) error {
    return db.Transaction(func(tx *gorm.DB) error {
        var fromAccount, toAccount Account
        
        if err := tx.First(&fromAccount, fromID).Error; err != nil {
            return err
        }
        if err := tx.First(&toAccount, toID).Error; err != nil {
            return err
        }
        
        if fromAccount.Balance < amount {
            return errors.New("余额不足")
        }
        
        if err := tx.Model(&fromAccount).Update("balance", gorm.Expr("balance - ?", amount)).Error; err != nil {
            return err
        }
        
        if err := tx.Model(&toAccount).Update("balance", gorm.Expr("balance + ?", amount)).Error; err != nil {
            return err
        }
        
        transfer := Transfer{
            FromID: fromID,
            ToID:   toID,
            Amount: amount,
        }
        return tx.Create(&transfer).Error
    })
}

订单创建

func CreateOrder(db *gorm.DB, order Order, items []OrderItem) error {
    return db.Transaction(func(tx *gorm.DB) error {
        if err := tx.Create(&order).Error; err != nil {
            return err
        }
        
        for i := range items {
            items[i].OrderID = order.ID
        }
        
        if err := tx.Create(&items).Error; err != nil {
            return err
        }
        
        for _, item := range items {
            var product Product
            if err := tx.First(&product, item.ProductID).Error; err != nil {
                return err
            }
            if product.Stock < item.Quantity {
                return errors.New("库存不足")
            }
            if err := tx.Model(&product).Update("stock", gorm.Expr("stock - ?", item.Quantity)).Error; err != nil {
                return err
            }
        }
        
        return nil
    })
}

用户注册

func Register(db *gorm.DB, user User, profile Profile) error {
    return db.Transaction(func(tx *gorm.DB) error {
        if err := tx.Create(&user).Error; err != nil {
            return err
        }
        
        profile.UserID = user.ID
        if err := tx.Create(&profile).Error; err != nil {
            return err
        }
        
        return nil
    })
}

事务传播

GORM 没有像 Spring 那样的事务传播机制。如果需要在事务中调用其他函数:

func CreateOrderWithItems(tx *gorm.DB, order Order, items []OrderItem) error {
    if tx == nil {
        return db.Transaction(func(t *gorm.DB) error {
            return CreateOrderWithItems(t, order, items)
        })
    }
    
    if err := tx.Create(&order).Error; err != nil {
        return err
    }
    
    for i := range items {
        items[i].OrderID = order.ID
    }
    
    return tx.Create(&items).Error
}

常见问题

忘记提交或回滚

tx := db.Begin()
tx.Create(&user)
// 忘记 Commit 或 Rollback

连接会一直被占用,导致连接泄漏。务必确保每个 Begin 都有对应的 Commit 或 Rollback。

在事务外操作

tx := db.Begin()
tx.Create(&user)
db.Create(&order) // 错误:用 db 而不是 tx
tx.Commit()

order 不在事务中,事务回滚时它不会回滚。

事务超时

长时间事务会占用资源,设置超时:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
    // 事务操作
})

小结

事务是数据一致性的保障。掌握手动事务和闭包事务两种方式,理解隔离级别的影响,注意错误处理和资源释放。