Go语言并发转账事务异常:仅首笔交易生效求助
问题排查与解决方案
针对你遇到的并发转账仅首笔成功的问题,大概率是并发事务中的丢失更新问题,结合PostgreSQL和Go的使用场景,常见原因和解决思路如下:
常见根因
- 未对账户行加锁:并发转账时,多个事务同时读取同一账户余额,修改后写回,导致后提交的事务覆盖前一个的更新,出现丢失更新。
- 事务隔离级别过低:默认的
READ COMMITTED隔离级别在高并发下无法防止这类问题。 - sqlc生成的更新语句未做行锁定:如果账户余额更新语句没有使用
FOR UPDATE或FOR NO KEY UPDATE,无法在事务中锁定目标行。
具体排查与修复步骤
1. 检查账户更新SQL语句
打开account_sql.go(或对应的sqlc定义文件),确认更新余额的语句是否带有行锁:
- 错误示例:
UPDATE accounts SET balance = balance + $1 WHERE id = $2
- 正确示例(添加行锁):
UPDATE accounts SET balance = balance + $1 WHERE id = $2 FOR NO KEY UPDATE
FOR NO KEY UPDATE会锁定目标账户行,阻止其他事务修改该行,直到当前事务提交/回滚,避免并发更新冲突。
2. 验证事务逻辑
检查store.go中的转账事务流程,确保:
- 事务开启后,所有操作(扣减余额、增加余额、创建交易记录)都在同一个事务中执行
- 事务执行失败时正确回滚
- 通过带锁的更新语句锁定转出和转入账户
示例正确的事务流程:
func (s *Store) TransferTx(ctx context.Context, arg TransferTxParams) (TransferTxResult, error) { var result TransferTxResult err := s.execTx(ctx, func(q *Queries) error { var err error // 扣减转出账户余额并锁定行 _, err = q.UpdateAccountBalance(ctx, UpdateAccountBalanceParams{ ID: arg.FromAccountID, Amount: -arg.Amount, }) if err != nil { return err } // 增加转入账户余额并锁定行 _, err = q.UpdateAccountBalance(ctx, UpdateAccountBalanceParams{ ID: arg.ToAccountID, Amount: arg.Amount, }) if err != nil { return err } // 创建交易记录 result.Transfer, err = q.CreateTransfer(ctx, CreateTransferParams{ FromAccountID: arg.FromAccountID, ToAccountID: arg.ToAccountID, Amount: arg.Amount, }) if err != nil { return err } return nil }) return result, err }
3. 检查PostgreSQL锁状态
执行以下SQL查询当前锁状态,确认是否存在长期持有的锁或未提交的事务:
-- 查询等待中的锁 SELECT * FROM pg_locks WHERE NOT granted; -- 查询空闲但未结束的事务 SELECT * FROM pg_stat_activity WHERE state = 'idle in transaction';
如果存在长期未提交的事务,会导致锁无法释放,阻塞后续操作。
4. 调整事务隔离级别(可选)
如果加行锁后仍有问题,可以尝试将事务隔离级别提升为REPEATABLE READ:
tx, err := s.db.BeginTx(ctx, &sql.TxOptions{ Isolation: sql.LevelRepeatableRead, }) if err != nil { return err } // 后续事务逻辑...
测试验证
修改代码后,编写并发测试用例(参考store_test.go),模拟多协程同时转账的场景,检查余额是否正确更新:
func TestTransferTx(t *testing.T) { store := NewStore(testDB) account1 := createRandomAccount(t) account2 := createRandomAccount(t) n := 5 amount := int64(10) var wg sync.WaitGroup wg.Add(n) for i := 0; i < n; i++ { go func() { defer wg.Done() _, err := store.TransferTx(context.Background(), TransferTxParams{ FromAccountID: account1.ID, ToAccountID: account2.ID, Amount: amount, }) require.NoError(t, err) }() } wg.Wait() // 校验最终余额 updatedAccount1, err := store.GetAccount(context.Background(), account1.ID) require.NoError(t, err) require.Equal(t, account1.Balance-int64(n)*amount, updatedAccount1.Balance) updatedAccount2, err := store.GetAccount(context.Background(), account2.ID) require.NoError(t, err) require.Equal(t, account2.Balance+int64(n)*amount, updatedAccount2.Balance) }
内容的提问来源于stack exchange,提问作者Sajna Sheeja
相关产品推荐
相关产品推荐

