事务管理
核心概念
事务是数据库操作的"原子单元"------要么全部成功,要么全部回滚。经典的 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
要点总结
db.Begin()返回*sql.Tx,Tx 上的所有操作绑定到同一连接,这是事务原子性的底层保证defer tx.Rollback()是黄金模式------成功时 Commit 后 Rollback 被忽略,失败时自动回滚,无需手动判断BeginTx(ctx, opts)支持隔离级别和 context 超时控制,生产环境推荐用这个版本- Savepoint 实现部分回滚 :
SAVEPOINT→ 操作 →ROLLBACK TO SAVEPOINT→RELEASE SAVEPOINT - 事务内必须做业务校验(如余额检查),校验失败直接 return error,defer 负责回滚
- 事务粒度要小------事务持有连接,长事务会占用连接池资源,导致其他请求阻塞