This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/GORM/08-事务管理.md
T
2026-04-28 20:23:33 +08:00

313 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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["不需要事务<br/>直接操作即可"]
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-性能优化]]