perf(wallet): 避免同钱包预留占满数据库池

千并发验收的 pg_stat_activity 显示大量事务锁等待集中在同一验收钱包,等待行锁的请求持续占用 API Pool,导致 canceled acquire 和排队增长。

在每个 Store 内增加 256 槽钱包预留分片锁,同一钱包先在进程内串行再申请数据库连接;跨 API 节点仍由 PostgreSQL 行锁保证余额与幂等正确性。

验证:Go 全量测试、临时 PostgreSQL 并发账务集成测试、gofmt 和 git diff --check 通过。
This commit is contained in:
2026-07-31 03:55:22 +08:00
parent 36546bd5c4
commit add80f781e
3 changed files with 47 additions and 1 deletions
+20
View File
@@ -5,14 +5,30 @@ import (
"encoding/json"
"errors"
"fmt"
"hash/fnv"
"strconv"
"strings"
"sync"
"time"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
"github.com/jackc/pgx/v5"
)
const walletReservationLockStripes = 256
type walletReservationLockSet struct {
stripes [walletReservationLockStripes]sync.Mutex
}
func (locks *walletReservationLockSet) lock(gatewayUserID string) func() {
hasher := fnv.New32a()
_, _ = hasher.Write([]byte(strings.TrimSpace(gatewayUserID)))
stripe := &locks.stripes[hasher.Sum32()%walletReservationLockStripes]
stripe.Lock()
return stripe.Unlock
}
type GatewayWalletAccount struct {
ID string `json:"id"`
GatewayTenantID string `json:"gatewayTenantId,omitempty"`
@@ -153,12 +169,16 @@ func (s *Store) ReserveTaskBilling(ctx context.Context, task GatewayTask, user *
pricingSnapshot = emptyObjectIfNil(pricingSnapshots[0])
}
if exactAmount := walletString(pricingSnapshot["reservationAmount"]); exactAmount != "" {
unlock := s.walletReservationLocks.lock(gatewayUserID)
defer unlock()
return s.reserveTaskBillingExact(ctx, task, gatewayUserID, exactAmount, pricingSnapshot)
}
amounts := walletBillingAmounts(billings)
if len(amounts) == 0 {
return nil, nil
}
unlock := s.walletReservationLocks.lock(gatewayUserID)
defer unlock()
reservations := make([]WalletBillingReservation, 0, len(amounts))
pricingSnapshotJSON, _ := json.Marshal(sanitizeJSONForStorage(pricingSnapshot))