事务处理
问题
Go 中如何正确管理数据库事务?有哪些最佳实践?
答案
database/sql 事务
func transfer(ctx context.Context, db *sql.DB, from, to int, amount float64) error {
tx, err := db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelReadCommitted, // 隔离级别
})
if err != nil {
return err
}
// 确保事务不会挂起
defer tx.Rollback()
// 扣款
_, err = tx.ExecContext(ctx,
"UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?",
amount, from, amount,
)
if err != nil {
return err
}
// 入账
_, err = tx.ExecContext(ctx,
"UPDATE accounts SET balance = balance + ? WHERE id = ?",
amount, to,
)
if err != nil {
return err
}
return tx.Commit()
}
事务封装模式
// 通用事务包装函数
func WithTransaction(ctx context.Context, db *sql.DB, fn func(tx *sql.Tx) error) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
if err := fn(tx); err != nil {
return err
}
return tx.Commit()
}
// 使用
err := WithTransaction(ctx, db, func(tx *sql.Tx) error {
// 事务内的操作
_, err := tx.ExecContext(ctx, "INSERT ...", args...)
return err
})
GORM 事务
// 自动事务(推荐)
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&order).Error; err != nil {
return err // 自动回滚
}
if err := tx.Create(&orderItems).Error; err != nil {
return err // 自动回滚
}
return nil // 自动提交
})
// 嵌套事务(使用 SavePoint)
db.Transaction(func(tx *gorm.DB) error {
tx.Create(&user)
tx.Transaction(func(tx2 *gorm.DB) error {
tx2.Create(&post) // SavePoint
return errors.New("inner fail") // 只回滚 post
})
return nil // user 仍然提交
})
事务最佳实践
事务注意事项
- 事务尽量短——持有锁的时间越短越好
- 不要在事务中做网络调用——调用外部 API、发消息等应在事务外
- defer Rollback——确保异常时事务能正确回滚
- 注意读写顺序——先写后读可能更新锁升级
- Context 超时——设置合理的事务超时
常见面试问题
Q1: 事务中 defer tx.Rollback() 和 tx.Commit() 不冲突吗?
答案:不冲突。tx.Commit() 后再调用 tx.Rollback() 是 no-op(什么都不做,返回 sql.ErrTxDone)。这是一种防御性编程——确保无论执行路径如何,事务都不会挂起。
Q2: Go 如何实现分布式事务?
答案:Go 没有内置分布式事务支持。常用方案:
- Saga 模式:每步操作搭配补偿操作
- 本地消息表:事务 + 消息表保证一致性
- DTM 框架:Go 的分布式事务框架,支持 TCC、Saga、XA