perf: remove paid result account lock contention

This commit is contained in:
hectorzhao
2026-08-21 10:40:28 +08:00
parent 487b5282a6
commit fcf6d3e6b4
6 changed files with 58 additions and 48 deletions
+42 -37
View File
@@ -441,44 +441,49 @@ export class BillingService {
if (amountCents <= 0) return null;
const releaseKey = `sms-charge-release:${data.messageId}`;
const chargeKey = `sms-charge:${data.messageId}`;
return this.prisma.$transaction(async (tx) => {
await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtextextended(${'tenant-account:' + data.tenantId}, 0))`;
const [release, charge] = await Promise.all([
tx.accountTransaction.findUnique({ where: { idempotencyKey: releaseKey } }),
tx.accountTransaction.findUnique({ where: { idempotencyKey: chargeKey } }),
]);
if (charge) return charge;
const account = await tx.tenantAccount.findUniqueOrThrow({ where: { tenantId: data.tenantId } });
const balance = moneyToNumber(account.balanceCents);
if (release) {
const updated = await tx.tenantAccount.update({
where: { tenantId: data.tenantId },
data: { balanceCents: { decrement: amountCents } },
});
return tx.accountTransaction.create({
data: {
tenantId: data.tenantId, transactionType: 'charged', idempotencyKey: chargeKey,
amountCents: -amountCents, balanceAfter: updated.balanceCents,
relatedType: 'sms_message_record', relatedId: data.messageId, remark: '提交成功扣费',
},
});
}
await tx.accountTransaction.createMany({
data: [
{
tenantId: data.tenantId, transactionType: 'released', idempotencyKey: releaseKey,
amountCents, balanceAfter: balance + amountCents,
relatedType: 'sms_batch_task', relatedId: data.taskId, remark: data.remark,
},
{
tenantId: data.tenantId, transactionType: 'charged', idempotencyKey: chargeKey,
amountCents: -amountCents, balanceAfter: balance,
relatedType: 'sms_message_record', relatedId: data.messageId, remark: '提交成功扣费',
},
],
const existing = await this.prisma.accountTransaction.findMany({
where: { idempotencyKey: { in: [releaseKey, chargeKey] } },
});
const charge = existing.find((row) => row.idempotencyKey === chargeKey);
if (charge) return charge;
const release = existing.find((row) => row.idempotencyKey === releaseKey);
if (release) {
// Recover the legacy two-transaction boundary: a crash may have committed
// release before charge, so this path must perform the missing balance debit.
return this.applyAccountDelta({
tenantId: data.tenantId, transactionType: 'charged', idempotencyKey: chargeKey,
amountCents: -amountCents, relatedType: 'sms_message_record', relatedId: data.messageId,
remark: '提交成功扣费(恢复既有已释放冻结)',
});
return tx.accountTransaction.findUniqueOrThrow({ where: { idempotencyKey: chargeKey } });
}, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted });
}
const rows = await this.prisma.$queryRaw<Array<{ id: string; idempotencyKey: string }>>(Prisma.sql`
WITH account AS (
SELECT "balanceCents" FROM "TenantAccount" WHERE "tenantId" = ${data.tenantId}
), inserted AS (
INSERT INTO "AccountTransaction" (
id, "tenantId", "transactionType", "idempotencyKey", "amountCents",
"balanceAfter", "relatedType", "relatedId", remark, "createdAt"
)
SELECT gen_random_uuid()::text, ${data.tenantId}, ledger."transactionType", ledger."idempotencyKey",
ledger."amountCents", account."balanceCents" + ledger."balanceDelta",
ledger."relatedType", ledger."relatedId", ledger.remark, (NOW() AT TIME ZONE 'UTC')
FROM account
CROSS JOIN (VALUES
('released', ${releaseKey}, ${amountCents}::bigint, ${amountCents}::bigint, 'sms_batch_task', ${data.taskId}, ${data.remark ?? null}),
('charged', ${chargeKey}, ${-amountCents}::bigint, 0::bigint, 'sms_message_record', ${data.messageId}, '提交成功扣费')
) AS ledger("transactionType", "idempotencyKey", "amountCents", "balanceDelta", "relatedType", "relatedId", remark)
ON CONFLICT ("idempotencyKey") DO NOTHING
RETURNING id, "idempotencyKey"
)
SELECT id, "idempotencyKey" FROM inserted WHERE "idempotencyKey" = ${chargeKey}
UNION ALL
SELECT id, "idempotencyKey" FROM "AccountTransaction" WHERE "idempotencyKey" = ${chargeKey}
LIMIT 1
`);
if (rows[0]) return rows[0];
// A concurrent identical callback can win ON CONFLICT while remaining
// invisible to this statement's snapshot; one read repairs that MVCC edge.
return this.prisma.accountTransaction.findUniqueOrThrow({ where: { idempotencyKey: chargeKey } });
}
release(data: BillingActionDto) {