← 中文资源

简体中文版本

SAVEPOINT 与嵌套事务

savepoint 在事务内部标记中间位置,部分回滚可以退回该点:单步失败只撤销该步,不必放弃整个事务。

阅读英文原文

概念定义

事务在顶层是全有或全无的。savepoint 在内部刻出检查点:失败的子步骤退回检查点,外围工作得以幸存。这就是“导入一万行、跳过一行坏数据而不丢九千九百九十九行”的实现方式。

工作机制

SAVEPOINT 建立命名标记,ROLLBACK TO SAVEPOINT 撤销其后工作、保留之前获得的锁,RELEASE SAVEPOINT 释放名字。PostgreSQL 出错只中止当前语句,捕获后继续很自然;Oracle 与 SQL Server 有类似机制但事务域细节不同。

使用场景

逐条容错的批量装载、后续步骤可跳过的复合操作、把每个用例包在回滚里的测试框架。建议逐条编号 savepoint 并定期 release 以约束状态规模。

常见误区

savepoint 是同一事务内的位置,不是独立事务,提交语义仍是整事务级的;它也只影响创建它的那条连接。部分驱动或严格模式在出错时直接中止整个事务,回退点形同虚设——需按栈验证。savepoint 之后获取的锁要到外层事务结束才释放;深层嵌套不释放会劣化性能与内存。

代码示例

from collections.abc import Iterator
from contextlib import contextmanager
from dataclasses import dataclass, field
from typing import Any


class TransactionAborted(RuntimeError):
    pass


@dataclass
class Store:
    data: dict[str, Any] = field(default_factory=dict)
    commits: int = 0
    rollbacks: int = 0

    @contextmanager
    def transaction(self) -> Iterator["Transaction"]:
        snapshot = dict(self.data)
        tx = Transaction(self)
        try:
            yield tx
        except BaseException:
            self.data = snapshot
            self.rollbacks += 1
            raise
        else:
            self.commits += 1


@dataclass
class Transaction:
    store: Store

    def set(self, key: str, value: Any) -> None:
        self.store.data[key] = value

    def delete(self, key: str) -> None:
        self.store.data.pop(key, None)


def main() -> None:
    store = Store(data={"balance": 100})
    with store.transaction() as tx:
        tx.set("balance", 80)
        tx.set("currency", "USD")
    print(store.data, store.commits, store.rollbacks)

    try:
        with store.transaction() as tx:
            tx.set("balance", 0)
            raise TransactionAborted("payment declined")
    except TransactionAborted as error:
        print("rolled back:", error)
    print(store.data, store.commits, store.rollbacks)


if __name__ == "__main__":
    main()
package main

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"time"
)

var ErrConflict = errors.New("serialization conflict")

func WithTx(
	ctx context.Context,
	db *sql.DB,
	attempts int,
	work func(*sql.Tx) error,
) error {
	if attempts < 1 {
		attempts = 1
	}
	var last error
	for attempt := 0; attempt < attempts; attempt++ {
		tx, err := db.BeginTx(ctx, nil)
		if err != nil {
			return err
		}
		err = work(tx)
		if err == nil {
			if commitErr := tx.Commit(); commitErr == nil {
				return nil
			} else {
				last = commitErr
			}
		} else {
			_ = tx.Rollback()
			last = err
		}
		if !errors.Is(last, ErrConflict) {
			return last
		}
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-time.After(time.Duration(attempt+1) * 50 * time.Millisecond):
		}
	}
	return fmt.Errorf("transaction failed after %d attempts: %w", attempts, last)
}

func transfer(ctx context.Context, db *sql.DB, from, to, amount int64) error {
	return WithTx(ctx, db, 3, func(tx *sql.Tx) error {
		result, err := tx.ExecContext(
			ctx,
			"UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?",
			amount, from, amount,
		)
		if err != nil {
			return err
		}
		affected, err := result.RowsAffected()
		if err != nil {
			return err
		}
		if affected == 0 {
			return errors.New("insufficient funds")
		}
		_, err = tx.ExecContext(
			ctx,
			"UPDATE accounts SET balance = balance + ? WHERE id = ?",
			amount, to,
		)
		return err
	})
}

func main() {
	fmt.Println(transfer != nil, ErrConflict)
}