功能说明
在可串行化隔离级别或高并发写入场景下,数据库可能返回序列化冲突,事务需要整体重试,而不是只重试失败的那条语句。手写重试容易出现忘记回滚、在已提交事务上继续操作、或对不可重试错误反复尝试等问题。WithTx 把开始事务、执行工作、提交或回滚统一封装,只在错误被判定为冲突时重试,其余错误立即返回。
transfer 演示真实用法:先尝试扣减余额,条件中包含余额充足判断;受影响行数为零说明余额不足,直接返回业务错误。扣减成功后再增加对方余额,两步处于同一事务,任何一步失败都会回滚。
关键代码
WithTx 把尝试次数下限校正为一,保存最后一次错误。每轮开始新事务,工作函数返回错误时回滚并记录;成功时提交,提交失败也进入错误处理。errors.Is 用来兼容被包装的错误,只有冲突才继续循环。退避时间随尝试次数线性增加,select 同时监听上下文取消,避免服务关闭时还在等待。最终错误用 %w 包装,保留原始错误链。transfer 使用参数占位符,杜绝拼接 SQL。
使用注意
重试必须保证工作函数可重复执行,不能在函数内发送邮件、调用外部支付等有副作用且不可回滚的操作。冲突判定应基于驱动提供的错误码或可识别错误,全部错误重试会放大故障。事务持有连接和锁,退避期间应结束事务,本例在每轮重新开始,符合要求。提交失败的结果不确定,客户端需要幂等键配合。重试次数和总时限应受请求预算约束,不能无限重试。
验证方式
该片段的主函数只用于展示函数签名与错误值,无法脱离数据库运行。接入真实数据库后,可让两个事务并发更新同一行,人为制造序列化冲突,观察日志中至少发生一次重试后成功。把工作函数改为返回普通错误,应只执行一次并立即回滚返回。
代码示例
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)
}