--- tags: [GORM, Go, ORM, 事务, Tx, Commit, Rollback, Nested Transaction] create time: 2026-04-28 00:00 --- # 事务管理 ## 概述 事务是数据库操作的「安全网」——它保证一组操作要么全部成功,要么全部失败。在现实业务中,你几乎处处离不开事务:转账时「扣 A 加 B」、下单时「减库存 + 创建订单 + 生成流水」,这些都需要事务来保证数据一致性。 ```mermaid flowchart TD Start[开始事务 tx = db.Begin] --> Op1["执行操作 1"] Op1 --> OK1{"操作 1 成功?"} OK1 --> |是| Op2["执行操作 2"] OK1 --> |否| Rollback["tx.Rollback()"] OK2{"操作 2 成功?"} --> |是| Op3["执行操作 3"] Op2 --> OK2 OK2 --> |否| Rollback Op3 --> OK3{"操作 3 成功?"} OK3 --> |是| Commit["tx.Commit()"] OK3 --> |否| Rollback style Start fill:#4FC08D,color:#fff style Commit fill:#3B82F6,color:#fff style Rollback fill:#EF4444,color:#fff ``` > [!definition] 事务四大特性(ACID) | 特性 | 含义 | 示例 | |------|------|------| | **原子性(Atomicity)** | 全部提交或全部回滚 | 转账:A 扣钱和 B 加钱不可分割 | | **一致性(Consistency)** | 事务前后数据满足业务规则 | 转账前后总金额不变 | | **隔离性(Isolation)** | 并发事务互不干扰 | 两个用户同时下单不会超卖 | | **持久性(Durability)** | 提交后永不过期 | 断电后数据仍在 | ## 基本用法 ### Begin / Commit / Rollback ```go // 1. 开启事务 tx := db.Begin() defer func() { if r := recover(); r != nil { tx.Rollback() // panic 时确保回滚 } }() // 2. 所有操作通过 tx 执行 user := User{Name: "Alice", Age: 30} if err := tx.Create(&user).Error; err != nil { tx.Rollback() return err } order := Order{UserID: user.ID, Amount: 99.9} if err := tx.Create(&order).Error; err != nil { tx.Rollback() return err } // 3. 全部成功,提交 if err := tx.Commit().Error; err != nil { return err } return nil ``` > [!warning] defer Rollback 的注意事项 > `Commit()` 成功后再执行 `defer Rollback()` 会报错(事务已提交)。所以推荐**显式处理错误路径的回滚**,`defer` 只用于兜底 panic。 ### 简洁写法(Err 链式判断) ```go tx := db.Begin() if err := tx.Error; err != nil { return err } // ... 操作 ... if err := tx.Commit().Error; err != nil { tx.Rollback() return err } ``` > [!tip] 理解 tx.Transaction 返回值 > GORM 的 `Begin()` 返回 `*gorm.DB`,但内部包装了事务。这个实例只在事务范围内有效,不能跨事务使用。 ## 事务中的错误处理 ```go func TransferMoney(fromID, toID uint, amount float64) error { // 开启事务 tx := db.Begin() // 查入账和出账账户(锁行!) var fromAccount Account if err := tx.Set("gorm:query_option", "FOR UPDATE").First(&fromAccount, fromID).Error; err != nil { tx.Rollback() return fmt.Errorf("查询转出账户失败: %w", err) } var toAccount Account if err := tx.First(&toAccount, toID).Error; err != nil { tx.Rollback() return fmt.Errorf("查询入账账户失败: %w", err) } // 余额不足 if fromAccount.Balance < amount { tx.Rollback() return errors.New("余额不足") } // 扣款 fromAccount.Balance -= amount if err := tx.Save(&fromAccount).Error; err != nil { tx.Rollback() return fmt.Errorf("扣款失败: %w", err) } // 入账 toAccount.Balance += amount if err := tx.Save(&toAccount).Error; err != nil { tx.Rollback() return fmt.Errorf("入账失败: %w", err) } return tx.Commit().Error } ``` > [!important] FOR UPDATE 锁行 > 事务中的查询如果要用更新的数据做决策,必须用 `FOR UPDATE`(排他锁),否则可能读到未提交的旧数据: > ```go > // ❌ 竞态条件:两个并发请求可能都读到余额足够并扣款 > db.Where("id = ?", id).First(&account) > account.Balance -= 100 > db.Save(&account) > > // ✅ 加锁读取,另一个事务只能等待 > db.Set("gorm:query_option", "FOR UPDATE").Where("id = ?", id).First(&account) > ``` ## 嵌套事务 当代码结构有分层时,内部函数可能需要发起自己的事务。GORM 支持嵌套事务机制——内部实际上是保存点(Savepoint),而非真正的事务: ```go func CreateOrderWithAudit(tx *gorm.DB, order Order) error { // 检查外层是否已有事务 if !tx.IsTransaction() { // 外层不是事务,自己开一个 tx = db.Begin() defer func() { if v := recover(); v != nil { tx.Rollback() panic(v) } }() } // 保存点:如果内部失败可以回滚到这里,不影响外部 tx.SavePoint("create_order") // 主逻辑 if err := tx.Create(&order).Error; err != nil { tx.RollbackTo("create_order") // 回滚到保存点 return err } // 审计日志记录 audit := Audit{Table: "orders", Action: "create", RecordID: order.ID} if err := tx.Create(&audit).Error; err != nil { tx.RollbackTo("create_order") // 审计失败也可以回滚 return err } // 成功 —— 如果不外层事务则提交,否则由外层统一提交 if !tx.IsTransaction() { return tx.Commit().Error } return nil } ``` > [!tip] 嵌套事务的本质 > MySQL/PostgreSQL 不支持真正的嵌套事务,它们用的是 **Savepoint**。这意味着: > - 内部回滚只会回滚到最近的 savepoint,不会影响外层事务 > - 最终 `COMMIT` 仍然是一次性的,从外层事务发出 > - 如果外层事务 `ROLLBACK`,整个事务树(包括所有 savepoint)都会被回滚 ### 事务级别控制 ```go import "database/sql" tx := db.Session(&gorm.Session{ DryRun: false, // 模拟模式:构建 SQL 但不执行 }).Begin(&sql.TxOptions{ Isolation: sql.LevelRepeatableRead, // 设置隔离级别 ReadOnly: false, // 读写事务 }) // 或者针对特定查询 db.Set("gorm:prepare_stmt", true).Find(&users) // 预编译语句 ``` > [!example] 隔离级别速查 | 级别 | 脏读 | 不可重复读 | 幻读 | 性能影响 | |------|------|-----------|------|---------| | Read Uncommitted | ❌ 允许 | ❌ 允许 | ❌ 允许 | 最快 | | Read Committed | ✅ 阻止 | ❌ 允许 | ❌ 允许 | 快 | | Repeatable Read(MySQL 默认) | ✅ 阻止 | ✅ 阻止 | ⚠️ 部分阻止 | 中等 | | Serializable | ✅ 阻止 | ✅ 阻止 | ✅ 阻止 | 最慢 | ## 事务与中间件结合 在实际项目中,经常需要将事务与 Gin 等 Web 框架集成,实现自动事务管理: ```go // Gin 中间件:每个 HTTP 请求自带一个事务 func TransactionMiddleware(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { // 开始事务 tx := db.Begin() c.Set("db", tx) // 存入 Context defer func() { if p := recover(); p != nil { tx.Rollback() panic(p) // 重新抛出让上层捕获 } // 请求处理完毕且无错误,提交事务 if c.Writer.Status() >= 200 && c.Writer.Status() < 400 { tx.Commit() } else { tx.Rollback() } }() c.Next() } } // 在 handler 中使用 func CreateUser(c *gin.Context) { // 从 Context 获取当前事务 txVal, _ := c.Get("db") tx := txVal.(*gorm.DB) user := User{Name: c.PostForm("name")} if err := tx.Create(&user).Error; err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } c.JSON(201, user) } ``` ## 事务决策流程图 ```mermaid flowchart TD Start[需要多步数据操作] --> InTx{"是否在已有事务中?"} InTx --> |是| SaveQ{需要局部回滚能力_} InTx --> |否| NewTx{"操作数量?"} NewTx --> |单条 SQL| SimpleTx["不需要事务
直接操作即可"] NewTx --> |多条 SQL| BeginTx["db.Begin()"] SaveQ --> |需要| Savepoint["SavePoint('name')"] SaveQ --> |不需要| Continue["继续执行"] BeginTx --> Ops["依次执行各操作"] Continue --> Ops Savepoint --> Ops Ops --> AllOK{"全部成功?"} AllOK --> |是| Commit["Commit"] AllOK --> |否| RollBack["Rollback"] style Start fill:#4FC08D,color:#fff style Commit fill:#3B82F6,color:#fff style RollBack fill:#EF4444,color:#fff style SimpleTx fill:#A0AEC0,color:#fff ``` ## 常见坑点速查 | 问题 | 原因 | 解决方案 | |------|------|---------| | 忘记 Commit/Rollback | 漏了错误分支的回滚逻辑 | 对所有错误分支显式调用 Rollback | | 事务内查询被其他事务修改 | 没加 FOR UPDATE | 关键查询加 `Set("gorm:query_option", "FOR UPDATE")` | | 嵌套事务外层的 Commit 失效 | 内部使用了独立连接 | 始终复用同一个 `*gorm.DB` 对象 | | 长事务导致行锁堆积 | 事务中包含耗时操作(RPC、HTTP) | 将耗时操作移出事务范围 | | defer Rollback 导致已提交事务报错 | Commit 后再执行 deferred Rollback | 用 panic 兜底,正常路径不依赖 defer | ## 关联笔记 - [[01-安装与初始化]] - [[03-CRUD 操作]] - [[14-错误处理]] - [[15-性能优化]]