事务处理


第七章:事务处理

7.1 事务基础

事务是数据库操作的基本单元,具有 ACID 特性:

特性 说明
Atomicity(原子性) 事务中的所有操作要么全部成功,要么全部失败
Consistency(一致性) 事务执行前后数据库处于一致状态
Isolation(隔离性) 并发事务相互隔离
Durability(持久性) 事务提交后数据永久保存

7.2 自动事务(Transaction 方法)

GORM 的 Transaction 方法会自动处理提交和回滚:

err := db.Transaction(func(tx *gorm.DB) error {
    // 在事务中执行操作,使用 tx 而非 db
    if err := tx.Create(&user).Error; err != nil {
        return err  // 返回错误自动回滚
    }
    
    if err := tx.Create(&order).Error; err != nil {
        return err
    }
    
    return nil  // 返回 nil 自动提交
})

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

7.3 手动事务

需要更精细控制时使用手动事务:

// 开始事务
tx := db.Begin()

// 在事务中操作
if err := tx.Create(&user).Error; err != nil {
    tx.Rollback()  // 回滚
    return err
}

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

// 提交事务
if err := tx.Commit().Error; err != nil {
    return err
}

检查事务状态

tx := db.Begin()

// 检查是否在事务中
fmt.Println(tx.Statement.InTransaction)  // true

// 检查错误
tx.Create(&user)
if tx.Error != nil {
    tx.Rollback()
}

7.4 SavePoint(保存点)

用于大型事务中的部分回滚:

tx := db.Begin()

// 第一步:创建用户
if err := tx.Create(&user).Error; err != nil {
    tx.Rollback()
    return err
}

// 设置保存点
tx.SavePoint("after_user_create")

// 第二步:创建订单(可能失败但不影响用户创建)
if err := tx.Create(&order).Error; err != nil {
    tx.RollbackTo("after_user_create")  // 回滚到保存点
    // 继续其他操作
    return err
}

// 第三步:创建支付记录
if err := tx.Create(&payment).Error; err != nil {
    tx.Rollback()
    return err
}

tx.Commit()

7.5 嵌套事务

GORM 支持嵌套事务,内部使用 SavePoint 实现:

db.Transaction(func(tx *gorm.DB) error {
    tx.Create(&user1)
    
    // 嵌套事务
    tx.Transaction(func(tx2 *gorm.DB) error {
        tx2.Create(&user2)
        return nil  // 提交内层
    })
    
    // 再嵌套一层
    tx.Transaction(func(tx2 *gorm.DB) error {
        tx2.Create(&user3)
        return errors.New("rollback user3")  // 只回滚这层
    })
    
    tx.Create(&user4)
    return nil  // user1, user2, user4 提交,user3 回滚
})

7.6 在事务中使用函数

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

// 使用
if err := CreateUserWithOrder(db, &user, &order); err != nil {
    log.Fatal(err)
}

7.7 事务隔离级别

import "sql"

// 设置隔离级别
sqlDB, _ := db.DB()
sqlDB.SetConnMaxLifetime(time.Hour)

// 在事务开始时指定隔离级别
// MySQL
tx := db.Begin(&sql.TxOptions{
    Isolation: sql.LevelReadCommitted,  // 读已提交
})

// PostgreSQL
tx := db.Begin(&sql.TxOptions{
    Isolation: sql.LevelSerializable,   // 可串行化
})

隔离级别说明

级别 脏读 不可重复读 幻读
Read Uncommitted
Read Committed
Repeatable Read
Serializable

MySQL 默认:Repeatable Read
PostgreSQL 默认:Read Committed

7.8 事务中的查询

// 在事务中查询会读取未提交的数据
tx := db.Begin()

// 创建记录
tx.Create(&user)

// 在同一事务中可以查询到
var found User
tx.First(&found, user.ID)  // 能查到

// 外部查询查不到
db.First(&found, user.ID)  // 查不到(ErrRecordNotFound)

tx.Commit()

7.9 上下文与超时

// 使用上下文控制事务超时
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
    if err := tx.Create(&order).Error; err != nil {
        return err
    }
    // 长时间操作...
    time.Sleep(10 * time.Second)
    return nil
})
// 超时后自动回滚,返回 context.DeadlineExceeded

7.10 分布式事务(Two-Phase Commit)

GORM 本身不支持分布式事务,需配合中间件:

// 使用 DTMDriver 实现分布式事务(需安装 github.com/dtm-labs/dtm-driver-gorm)
import "github.com/dtm-labs/dtm/driver/gorm"

saga := dtmcli.NewSaga(dtmServerUrl, gid).
    Add("/api/transOut", "/api/transOutCompensate", &TransReq{}).
    Add("/api/transIn", "/api/transInCompensate", &TransReq{})

err := saga.Submit()

7.11 实战:转账系统

package main

import (
    "errors"
    "fmt"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

type Account struct {
    ID      uint   `gorm:"primaryKey"`
    UserID  uint   `gorm:"uniqueIndex"`
    Balance int64  // 单位为分,避免浮点数精度问题
    Version int    // 乐观锁版本号
}

// Transfer 转账
type Transfer struct {
    FromUserID uint
    ToUserID   uint
    Amount     int64
}

// TransferService 转账服务
type TransferService struct {
    db *gorm.DB
}

// Transfer 执行转账(使用事务)
func (s *TransferService) Transfer(t *Transfer) error {
    if t.Amount <= 0 {
        return errors.New("转账金额必须大于0")
    }
    
    return s.db.Transaction(func(tx *gorm.DB) error {
        var fromAccount, toAccount Account
        
        // 1. 获取转出账户(加锁)
        if err := tx.Set("gorm:query_option", "FOR UPDATE").
            First(&fromAccount, "user_id = ?", t.FromUserID).Error; err != nil {
            return fmt.Errorf("转出账户不存在: %w", err)
        }
        
        // 2. 检查余额
        if fromAccount.Balance < t.Amount {
            return errors.New("余额不足")
        }
        
        // 3. 获取转入账户
        if err := tx.Set("gorm:query_option", "FOR UPDATE").
            First(&toAccount, "user_id = ?", t.ToUserID).Error; err != nil {
            return fmt.Errorf("转入账户不存在: %w", err)
        }
        
        // 4. 扣款
        if err := tx.Model(&fromAccount).
            UpdateColumn("balance", gorm.Expr("balance - ?", t.Amount)).Error; err != nil {
            return err
        }
        
        // 5. 入账
        if err := tx.Model(&toAccount).
            UpdateColumn("balance", gorm.Expr("balance + ?", t.Amount)).Error; err != nil {
            return err
        }
        
        // 6. 记录转账日志(可选)
        log := TransferLog{
            FromUserID: t.FromUserID,
            ToUserID:   t.ToUserID,
            Amount:     t.Amount,
            Status:     1,
        }
        if err := tx.Create(&log).Error; err != nil {
            return err
        }
        
        return nil
    })
}

// TransferLog 转账日志
type TransferLog struct {
    ID         uint `gorm:"primaryKey"`
    FromUserID uint
    ToUserID   uint
    Amount     int64
    Status     int8 // 1-成功 2-失败
}

func main() {
    db, _ := gorm.Open(sqlite.Open("bank.db"), &gorm.Config{})
    db.AutoMigrate(&Account{}, &TransferLog{})
    
    // 创建测试账户
    db.Create(&Account{UserID: 1, Balance: 10000})  // 100元
    db.Create(&Account{UserID: 2, Balance: 5000})   // 50元
    
    service := &TransferService{db: db}
    
    // 执行转账
    err := service.Transfer(&Transfer{
        FromUserID: 1,
        ToUserID:   2,
        Amount:     3000,  // 转30元
    })
    
    if err != nil {
        fmt.Println("转账失败:", err)
    } else {
        fmt.Println("转账成功")
    }
    
    // 验证结果
    var acc1, acc2 Account
    db.First(&acc1, "user_id = ?", 1)
    db.First(&acc2, "user_id = ?", 2)
    fmt.Printf("账户1余额: %.2f元\n", float64(acc1.Balance)/100)
    fmt.Printf("账户2余额: %.2f元\n", float64(acc2.Balance)/100)
}

7.12 实战:订单处理流程

// OrderService 订单服务
type OrderService struct {
    db *gorm.DB
}

// CreateOrder 创建订单(包含库存扣减、优惠券使用等多个操作)
func (s *OrderService) CreateOrder(req *CreateOrderRequest) (*Order, error) {
    var order Order
    
    err := s.db.Transaction(func(tx *gorm.DB) error {
        // 1. 创建订单主记录
        order = Order{
            UserID:     req.UserID,
            TotalAmount: req.TotalAmount,
            Status:     OrderStatusPending,
        }
        if err := tx.Create(&order).Error; err != nil {
            return err
        }
        
        // 2. 创建订单项
        for _, item := range req.Items {
            orderItem := OrderItem{
                OrderID:   order.ID,
                ProductID: item.ProductID,
                Quantity:  item.Quantity,
                Price:     item.Price,
            }
            if err := tx.Create(&orderItem).Error; err != nil {
                return err
            }
            
            // 3. 扣减库存
            result := tx.Model(&Product{}).
                Where("id = ? AND stock >= ?", item.ProductID, item.Quantity).
                UpdateColumn("stock", gorm.Expr("stock - ?", item.Quantity))
            
            if result.RowsAffected == 0 {
                return fmt.Errorf("商品 %d 库存不足", item.ProductID)
            }
        }
        
        // 4. 使用优惠券
        if req.CouponID > 0 {
            result := tx.Model(&Coupon{}).
                Where("id = ? AND user_id = ? AND status = ?", 
                    req.CouponID, req.UserID, CouponStatusUnused).
                Update("status", CouponStatusUsed)
            
            if result.RowsAffected == 0 {
                return errors.New("优惠券无效或已使用")
            }
        }
        
        // 5. 设置保存点(后续操作可以独立回滚)
        tx.SavePoint("order_created")
        
        // 6. 尝试预扣积分(失败不影响订单创建)
        if req.UsePoints > 0 {
            if err := s.deductPoints(tx, req.UserID, req.UsePoints); err != nil {
                // 积分扣除失败,回滚到保存点,但保留订单
                tx.RollbackTo("order_created")
                // 记录日志但不返回错误
            }
        }
        
        return nil
    })
    
    return &order, err
}

func (s *OrderService) deductPoints(tx *gorm.DB, userID uint, points int) error {
    // 积分扣除逻辑
    return tx.Model(&UserPoints{}).
        Where("user_id = ? AND balance >= ?", userID, points).
        UpdateColumn("balance", gorm.Expr("balance - ?", points)).Error
}

7.13 最佳实践

1. 事务范围最小化

// 好:只把必要的操作放入事务
processData(data)  // 不需要事务
db.Transaction(func(tx *gorm.DB) error {
    return tx.Create(&record).Error  // 需要事务
})
sendNotification()  // 不需要事务

2. 避免在事务中调用外部服务

// 坏:事务中调用 HTTP 服务,可能导致长时间持有锁
err := db.Transaction(func(tx *gorm.DB) error {
    tx.Create(&order)
    resp, _ := http.Post(...)  // 不要这样做!
    return nil
})

// 好:先准备数据,再开启事务
data := prepareData()
resp, _ := http.Post(...)  // 外部调用在事务外
err := db.Transaction(func(tx *gorm.DB) error {
    return tx.Create(&data).Error
})

3. 处理事务中的 panic

func SafeTransaction(db *gorm.DB, fn func(tx *gorm.DB) error) (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panic in transaction: %v", r)
        }
    }()
    
    return db.Transaction(fn)
}

7.14 练习题

  1. 实现一个银行批量转账功能,要求:任意一笔失败则全部回滚,但记录失败原因
  2. 使用 SavePoint 实现订单创建流程:订单创建成功后,优惠券使用失败可独立回滚
  3. 编写一个支持超时的转账函数,超时后自动取消事务

7.15 小结

本章详细讲解了 GORM 的事务处理机制,包括自动事务、手动事务、SavePoint 和嵌套事务。正确使用事务是保证数据一致性的关键,要注意控制事务范围和避免长时间事务。


本文代码地址:https://github.com/LittleMoreInteresting/gorm_study

欢迎关注公众号,一起学习进步!

如有疑问关注公众号给我留言
wx

关注公众号

©2017-2023 鲁ICP备17023316号-1 Powered by Hugo