事务最佳实践

事务是数据一致性的保障,但使用不当会带来性能问题和死锁风险。掌握最佳实践,才能用好事务。

事务要短小

事务占用数据库资源,时间越长,风险越大:

// 错误:事务中调用外部服务
func CreateOrder(db *gorm.DB, order Order) error {
    return db.Transaction(func(tx *gorm.DB) error {
        tx.Create(&order)
        
        // 调用支付接口,可能很慢
        if err := paymentService.Pay(order); err != nil {
            return err
        }
        
        return nil
    })
}

// 正确:先完成数据库操作,再调用外部服务
func CreateOrder(db *gorm.DB, order Order) error {
    err := db.Transaction(func(tx *gorm.DB) error {
        return tx.Create(&order).Error
    })
    if err != nil {
        return err
    }
    
    return paymentService.Pay(order)
}

避免长事务

长事务的危害:

  • 占用连接,影响并发
  • 锁定资源,阻塞其他操作
  • 回滚代价大
  • 死锁风险高
// 错误:批量操作放在一个事务
func BatchImport(db *gorm.DB, users []User) error {
    return db.Transaction(func(tx *gorm.DB) error {
        for _, user := range users {
            if err := tx.Create(&user).Error; err != nil {
                return err
            }
        }
        return nil
    })
}

// 正确:分批处理
func BatchImport(db *gorm.DB, users []User) error {
    batchSize := 100
    for i := 0; i < len(users); i += batchSize {
        end := i + batchSize
        if end > len(users) {
            end = len(users)
        }
        
        batch := users[i:end]
        if err := db.Transaction(func(tx *gorm.DB) error {
            return tx.Create(&batch).Error
        }); err != nil {
            return err
        }
    }
    return nil
}

锁顺序一致

死锁的常见原因是锁顺序不一致:

// 可能死锁
func Transfer(db *gorm.DB, fromID, toID uint, amount float64) error {
    return db.Transaction(func(tx *gorm.DB) error {
        var from, to Account
        tx.First(&from, fromID)
        tx.First(&to, toID)
        
        from.Balance -= amount
        to.Balance += amount
        
        tx.Save(&from)
        tx.Save(&to)
        return nil
    })
}

如果 A 转给 B 和 B 转给 A 同时执行,可能死锁。

解决方案:按固定顺序锁定:

func Transfer(db *gorm.DB, fromID, toID uint, amount float64) error {
    return db.Transaction(func(tx *gorm.DB) error {
        ids := []uint{fromID, toID}
        sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
        
        var accounts []Account
        tx.Clauses(clause.Locking{Strength: "UPDATE"}).
            Where("id IN ?", ids).
            Find(&accounts)
        
        // 处理转账...
        return nil
    })
}

设置超时

事务要有超时限制:

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

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

超时后事务会自动回滚。

错误处理

正确处理事务错误:

func CreateOrder(db *gorm.DB, order Order) error {
    tx := db.Begin()
    defer func() {
        if r := recover(); r != nil {
            tx.Rollback()
        }
    }()
    
    if err := tx.Create(&order).Error; err != nil {
        tx.Rollback()
        return err
    }
    
    if err := tx.Commit().Error; err != nil {
        return err
    }
    
    return nil
}

或者用闭包:

func CreateOrder(db *gorm.DB, order Order) error {
    return db.Transaction(func(tx *gorm.DB) error {
        return tx.Create(&order).Error
    })
}

只读查询不加事务

只读操作不需要事务:

// 不必要的事务
func GetUser(db *gorm.DB, id uint) (*User, error) {
    var user User
    err := db.Transaction(func(tx *gorm.DB) error {
        return tx.First(&user, id).Error
    })
    return &user, err
}

// 直接查询
func GetUser(db *gorm.DB, id uint) (*User, error) {
    var user User
    err := db.First(&user, id).Error
    return &user, err
}

选择合适的隔离级别

不同场景选择不同隔离级别:

// 报表统计:读已提交即可
db.Begin(&sql.TxOptions{
    Isolation: sql.LevelReadCommitted,
})

// 金融交易:可重复读
db.Begin(&sql.TxOptions{
    Isolation: sql.LevelRepeatableRead,
})

// 高一致性要求:串行化
db.Begin(&sql.TxOptions{
    Isolation: sql.LevelSerializable,
})

隔离级别越高,并发性能越低。

重试机制

某些错误可以重试:

func WithRetry(db *gorm.DB, fn func(tx *gorm.DB) error, maxRetries int) error {
    for i := 0; i < maxRetries; i++ {
        err := db.Transaction(fn)
        if err == nil {
            return nil
        }
        
        if isRetryableError(err) {
            time.Sleep(time.Duration(i+1) * 100 * time.Millisecond)
            continue
        }
        
        return err
    }
    return errors.New("重试次数耗尽")
}

func isRetryableError(err error) bool {
    return strings.Contains(err.Error(), "deadlock") ||
           strings.Contains(err.Error(), "lock wait timeout")
}

日志记录

事务操作要有日志:

func CreateOrder(db *gorm.DB, order Order) error {
    start := time.Now()
    
    err := db.Transaction(func(tx *gorm.DB) error {
        return tx.Create(&order).Error
    })
    
    duration := time.Since(start)
    if err != nil {
        log.Printf("事务失败: %v, 耗时: %v", err, duration)
    } else {
        log.Printf("事务成功, 耗时: %v", duration)
    }
    
    return err
}

监控指标

监控事务相关指标:

  • 事务执行时间
  • 事务成功率
  • 死锁次数
  • 等待锁时间
var transactionDuration = prometheus.NewHistogramVec(
    prometheus.HistogramOpts{
        Name: "transaction_duration_seconds",
        Help: "事务执行时间",
    },
    []string{"status"},
)

func MonitoredTransaction(db *gorm.DB, fn func(tx *gorm.DB) error) error {
    start := time.Now()
    err := db.Transaction(fn)
    duration := time.Since(start).Seconds()
    
    status := "success"
    if err != nil {
        status = "error"
    }
    
    transactionDuration.WithLabelValues(status).Observe(duration)
    return err
}

小结

事务是双刃剑,用好了保证一致性,用不好带来性能问题。记住:短小、有序、超时、监控。