Golang 事务管理

事务管理

核心概念

事务是数据库操作的"原子单元"------要么全部成功,要么全部回滚。经典的 ACID 四特性:

特性 含义 通俗理解
Atomicity(原子性) 事务内操作要么全做要么全不做 不做半截事
Consistency(一致性) 事务前后数据满足约束规则 钱不会凭空多出来
Isolation(隔离性) 并发事务互不干扰 各干各的别串台
Durability(持久性) 提交后永久保存 落袋为安

Go 中的事务 API

go 复制代码
tx, err := db.Begin()        // 开启事务
// ... 在 tx 上执行操作 ...
err = tx.Commit()            // 提交
// 或
err = tx.Rollback()          // 回滚

db.Begin() 返回的 *sql.Tx 拥有和 *sql.DB 完全相同的 Query/QueryRow/Exec/Prepare 方法,但所有操作都绑定到同一个底层连接上------这是事务能保证原子性的基础。

事务的黄金法则

go 复制代码
func transferMoney(db *sql.DB, fromID, toID int64, amount int) error {
    tx, err := db.Begin()
    if err != nil {
        return err
    }

    // 关键:defer 中调用 Rollback
    // 如果已经 Commit,Rollback 会被忽略(返回 sql.ErrTxDone)
    defer func() {
        _ = tx.Rollback()
    }()

    // 执行业务 SQL...
    if _, err := tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromID); err != nil {
        return err  // defer 会自动 Rollback
    }
    if _, err := tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, toID); err != nil {
        return err  // defer 会自动 Rollback
    }

    return tx.Commit()  // 成功才提交
}

为什么 defer Rollback 是安全的? Go 源码中 tx.Commit() 内部会先把事务标记为已完成,之后 Rollback() 发现事务已结束,返回 sql.ErrTxDone 并直接返回,不会影响已提交的数据。所以这个模式是官方推荐的惯用法。

事务隔离级别

go 复制代码
// 设置隔离级别
db.SetMaxOpenConns(1) // SQLite 串行化演示用

// Go 1.8+ 支持设置隔离级别
tx, err := db.BeginTx(ctx, &sql.TxOptions{
    Isolation: sql.LevelSerializable,
    ReadOnly:  false,
})
隔离级别 脏读 不可重复读 幻读 性能
Read Uncommitted 可能 可能 可能 最高
Read Committed(MySQL 默认) 不可能 可能 可能
Repeatable Read(MySQL InnoDB 默认) 不可能 不可能 可能*
Serializable 不可能 不可能 不可能 最低

*MySQL InnoDB 的 Repeatable Read 通过间隙锁解决了幻读,这是 MySQL 的扩展行为。

嵌套事务:Savepoint

标准 SQL 没有"嵌套事务",但有 Savepoint(保存点) 机制:

sql 复制代码
SAVEPOINT sp1
-- 执行一些操作
ROLLBACK TO SAVEPOINT sp1  -- 回滚到保存点,不回滚整个事务
RELEASE SAVEPOINT sp1       -- 释放保存点

Go 的 database/sql 没有直接封装 Savepoint,但可以通过 tx.Exec("SAVEPOINT sp1") 手动操作。

完整练习代码

go 复制代码
// transaction_management.go
package main

import (
	"context"
	"database/sql"
	"fmt"
	"log"

	_ "modernc.org/sqlite"
)

func main() {
	db, err := sql.Open("sqlite", ":memory:")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// 建表 + 初始化数据
	db.Exec(`CREATE TABLE accounts (
		id INTEGER PRIMARY KEY,
		name TEXT NOT NULL,
		balance INTEGER NOT NULL CHECK (balance >= 0)
	)`)
	db.Exec("INSERT INTO accounts(id, name, balance) VALUES(1, 'Alice', 1000)")
	db.Exec("INSERT INTO accounts(id, name, balance) VALUES(2, 'Bob', 500)")

	fmt.Println("=== 初始状态 ===")
	printBalances(db)

	// ============================================
	// 1. 正常转账事务(成功)
	// ============================================
	fmt.Println("\n=== 转账: Alice -> Bob, 200元 ===")
	if err := transfer(db, 1, 2, 200); err != nil {
		log.Printf("转账失败: %v", err)
	}
	printBalances(db)

	// ============================================
	// 2. 余额不足转账(回滚)
	// ============================================
	fmt.Println("\n=== 转账: Bob -> Alice, 99999元(余额不足) ===")
	if err := transfer(db, 2, 1, 99999); err != nil {
		fmt.Printf("  转账失败(预期): %v\n", err)
	}
	printBalances(db) // 余额不变

	// ============================================
	// 3. Savepoint 演示
	// ============================================
	fmt.Println("\n=== Savepoint 演示 ===")
	if err := savepointDemo(db); err != nil {
		log.Printf("Savepoint 操作失败: %v", err)
	}
	printBalances(db)

	// ============================================
	// 4. 带 context 的事务(超时控制)
	// ============================================
	fmt.Println("\n=== Context 超时控制 ===")
	ctx, cancel := context.WithTimeout(context.Background(), 1<<63-1) // 超大超时
	defer cancel()
	if err := transferWithCtx(ctx, db, 1, 2, 100); err != nil {
		log.Printf("转账失败: %v", err)
	}
	printBalances(db)

	// ============================================
	// 5. 批量操作事务:要么全成功,要么全回滚
	// ============================================
	fmt.Println("\n=== 批量转账事务 ===")
	transfers := []struct {
		from, to int64
		amount   int
	}{
		{1, 2, 50},
		{2, 1, 30},
		{1, 2, 20},
	}
	if err := batchTransfer(db, transfers); err != nil {
		log.Printf("批量转账失败: %v", err)
	}
	printBalances(db)
}

// transfer 转账事务:保证原子性
func transfer(db *sql.DB, fromID, toID int64, amount int) error {
	tx, err := db.Begin()
	if err != nil {
		return err
	}
	// 安全的 defer Rollback:Commit 后此调用被忽略
	defer func() { _ = tx.Rollback() }()

	// 检查余额是否充足
	var balance int
	err = tx.QueryRow("SELECT balance FROM accounts WHERE id = ?", fromID).Scan(&balance)
	if err != nil {
		return fmt.Errorf("查询余额失败: %w", err)
	}
	if balance < amount {
		return fmt.Errorf("余额不足: 当前 %d, 需要转出 %d", balance, amount)
	}

	// 扣款
	if _, err := tx.Exec(
		"UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromID,
	); err != nil {
		return fmt.Errorf("扣款失败: %w", err)
	}

	// 入账
	if _, err := tx.Exec(
		"UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, toID,
	); err != nil {
		return fmt.Errorf("入账失败: %w", err)
	}

	if err := tx.Commit(); err != nil {
		return fmt.Errorf("提交失败: %w", err)
	}
	return nil
}

// savepointDemo Savepoint 演示:部分回滚
func savepointDemo(db *sql.DB) error {
	tx, err := db.Begin()
	if err != nil {
		return err
	}
	defer func() { _ = tx.Rollback() }()

	// 操作1:Alice 存入 100
	tx.Exec("UPDATE accounts SET balance = balance + 100 WHERE id = 1")
	fmt.Println("  [Savepoint] Alice 存入 100")

	// 设置保存点
	tx.Exec("SAVEPOINT before_risky_op")

	// 操作2:一个有风险的操作(故意让它不影响最终结果)
	tx.Exec("UPDATE accounts SET balance = balance - 50 WHERE id = 1")
	fmt.Println("  [Savepoint] 风险操作: Alice 扣除 50")

	// 回滚到保存点(撤销操作2,保留操作1)
	tx.Exec("ROLLBACK TO SAVEPOINT before_risky_op")
	fmt.Println("  [Savepoint] 回滚到保存点,撤销风险操作")

	// 释放保存点
	tx.Exec("RELEASE SAVEPOINT before_risky_op")

	return tx.Commit()
}

// transferWithCtx 带 context 的事务
func transferWithCtx(ctx context.Context, db *sql.DB, fromID, toID int64, amount int) error {
	// BeginTx 支持传入 context 和事务选项
	tx, err := db.BeginTx(ctx, &sql.TxOptions{
		Isolation: sql.LevelDefault,
	})
	if err != nil {
		return err
	}
	defer func() { _ = tx.Rollback() }()

	// 使用 ExecContext 响应 context 取消
	if _, err := tx.ExecContext(ctx,
		"UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromID,
	); err != nil {
		return err
	}
	if _, err := tx.ExecContext(ctx,
		"UPDATE accounts SET balance = balance + ? WHERE id = ?", amount, toID,
	); err != nil {
		return err
	}

	return tx.Commit()
}

// batchTransfer 批量转账:全部成功才提交
func batchTransfer(db *sql.DB, transfers []struct {
	from, to int64
	amount   int
}) error {
	tx, err := db.Begin()
	if err != nil {
		return err
	}
	defer func() { _ = tx.Rollback() }()

	for i, t := range transfers {
		// 检查余额
		var balance int
		if err := tx.QueryRow("SELECT balance FROM accounts WHERE id = ?", t.from).Scan(&balance); err != nil {
			return fmt.Errorf("第%d笔: 查询余额失败: %w", i+1, err)
		}
		if balance < t.amount {
			return fmt.Errorf("第%d笔: 余额不足 (%d < %d)", i+1, balance, t.amount)
		}

		if _, err := tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", t.amount, t.from); err != nil {
			return fmt.Errorf("第%d笔: 扣款失败: %w", i+1, err)
		}
		if _, err := tx.Exec("UPDATE accounts SET balance = balance + ? WHERE id = ?", t.amount, t.to); err != nil {
			return fmt.Errorf("第%d笔: 入账失败: %w", i+1, err)
		}
		fmt.Printf("  第%d笔: %d -> %d, %d元\n", i+1, t.from, t.to, t.amount)
	}

	return tx.Commit()
}

func printBalances(db *sql.DB) {
	rows, err := db.Query("SELECT id, name, balance FROM accounts ORDER BY id")
	if err != nil {
		log.Fatal(err)
	}
	defer rows.Close()

	fmt.Printf("  当前余额: ")
	for rows.Next() {
		var id, balance int
		var name string
		rows.Scan(&id, &name, &balance)
		fmt.Printf("%s=%d  ", name, balance)
	}
	fmt.Println()
}

运行方式

bash 复制代码
mkdir -p demo3 && cd demo3
go mod init demo3
go get modernc.org/sqlite
go run main.go

要点总结

  1. db.Begin() 返回 *sql.Tx,Tx 上的所有操作绑定到同一连接,这是事务原子性的底层保证
  2. defer tx.Rollback() 是黄金模式------成功时 Commit 后 Rollback 被忽略,失败时自动回滚,无需手动判断
  3. BeginTx(ctx, opts) 支持隔离级别和 context 超时控制,生产环境推荐用这个版本
  4. Savepoint 实现部分回滚SAVEPOINT → 操作 → ROLLBACK TO SAVEPOINTRELEASE SAVEPOINT
  5. 事务内必须做业务校验(如余额检查),校验失败直接 return error,defer 负责回滚
  6. 事务粒度要小------事务持有连接,长事务会占用连接池资源,导致其他请求阻塞
相关推荐
ruleslol10 小时前
Redis 雪崩与服务降级
数据库·redis
橙橙笔记11 小时前
QT的安装
开发语言·qt·安装·软件
格林威11 小时前
C#图像像素放大:邻域平均、双线性插值实现像素放大的C#实现代码
开发语言·人工智能·数码相机·计算机视觉·c#·视觉检测·工业相机
兔兔兔兔111 小时前
记录C++ 8
开发语言·c++
易番番ERP11 小时前
品牌代理商SKU繁多,ERP如何高效处理新旧规格替换?
数据库·微服务·云原生·sku·易番番erp
leo_yu_yty11 小时前
Mysql 面试准备
数据库·mysql·面试
SelectDB12 小时前
拉卡拉统一金融 OLAP:基于 Apache Doris / SelectDB 实现查询提速 15 倍、资源直降 52%
数据库
名字还没想好☜12 小时前
Go 1.23 range-over-func 迭代器实战:自定义可迭代类型、提前退出与惰性求值
开发语言·后端·golang·go·迭代器
SelectDB12 小时前
四川航空湖仓一体:基于 SelectDB / Apache Doris 的多源数据联邦分析实践
数据库
SelectDB12 小时前
网易云音乐日志平台:基于 Apache Doris / SelectDB 替换 ClickHouse 承载日增万亿日志
数据库