事务处理
第七章:事务处理
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 练习题
- 实现一个银行批量转账功能,要求:任意一笔失败则全部回滚,但记录失败原因
- 使用 SavePoint 实现订单创建流程:订单创建成功后,优惠券使用失败可独立回滚
- 编写一个支持超时的转账函数,超时后自动取消事务
7.15 小结
本章详细讲解了 GORM 的事务处理机制,包括自动事务、手动事务、SavePoint 和嵌套事务。正确使用事务是保证数据一致性的关键,要注意控制事务范围和避免长时间事务。
本文代码地址:https://github.com/LittleMoreInteresting/gorm_study
欢迎关注公众号,一起学习进步!