← 中文资源

简体中文版本

PGX 序列化失败事务重试

检测 PostgreSQL 的 40001 序列化冲突并整块重放事务:行锁读取保证读改写一致,业务性错误不重试直接上抛。

阅读英文原文

示例说明

可串行化隔离级别下数据库主动检测冲突并回滚其中一个事务,这不是失败而是并发控制的正常反馈——正确响应是重放整个事务单元。BeginFunc 把事务体收拢成函数,任何返回错误自动回滚;40001 错误码被识别后按递增延迟重试,业务规则不满足的错误直接上抛不浪费预算。

技术要点

  • 40001 错误码识别序列化冲突
  • BeginFunc 函数式事务自动回滚
  • for update 行锁锁定读改写窗口

代码解读

事务体内先 select for update 锁定目标行再读余额——锁保证读到提交后的最新值;余额不足返回普通错误走失败路径。runWithRetry 捕获 BeginFunc 的错误分类处理:序列化冲突退避后 continue 重放全部语句,四次预算耗尽或其他错误原样返回。

使用建议

重试次数按业务时延预算设定。热点行冲突率高时考虑队列化削峰。事务体必须无副作用外泄,重放才能安全。

代码示例

package main

import (
	"context"
	"errors"
	"fmt"
	"time"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgconn"
	"github.com/jackc/pgx/v5/pgxpool"
)

func isSerializationFailure(err error) bool {
	var pgErr *pgconn.PgError
	return errors.As(err, &pgErr) && pgErr.Code == "40001"
}

func runWithRetry(ctx context.Context, pool *pgxpool.Pool, attempts int) (int, error) {
	var balance int

	for attempt := 1; attempt <= attempts; attempt++ {
		err := pgx.BeginFunc(ctx, pool, func(tx pgx.Tx) error {
			row := tx.QueryRow(ctx,
				"select balance from accounts where id = 1 for update")
			if err := row.Scan(&balance); err != nil {
				return fmt.Errorf("lock read: %w", err)
			}

			if balance < 100 {
				return errors.New("insufficient funds")
			}

			_, err := tx.Exec(ctx,
				"update accounts set balance = balance - 100 where id = 1")
			return err
		})

		if err == nil {
			return balance - 100, nil
		}
		if isSerializationFailure(err) && attempt < attempts {
			time.Sleep(time.Duration(attempt*50) * time.Millisecond)
			continue // 序列化冲突:整块重放
		}
		return 0, err
	}
	return 0, errors.New("unreachable")
}

func main() {
	config, _ := pgxpool.ParseConfig("postgres://user:pass@localhost/app")
	pool, err := pgxpool.NewWithConfig(context.Background(), config)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer pool.Close()

	final, err := runWithRetry(context.Background(), pool, 4)
	fmt.Println("final balance:", final, "err:", err)
}