fix(mining): resolve transaction timeout by using single transaction for system accounts
Problem: - Multiple concurrent transactions updating system_mining_accounts caused row lock contention - 16+ transactions waiting for tuple/transactionid locks led to timeout errors - This prevented writeMinuteRecords() from executing, leaving mining_records empty Solution: - Modified SystemMiningAccountRepository.mine() to accept optional external transaction client - Created new distributeToSystemAndPending() method that processes all system accounts and pending contributions in a single transaction - Pre-calculate all rewards before transaction, then execute updates sequentially - Aggregate all pending contribution rewards into single HEADQUARTERS update - Move Redis accumulation outside transaction to avoid blocking Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d5e5bf642c
commit
9deffe2565
|
|
@ -139,55 +139,17 @@ export class MiningDistributionService {
|
||||||
page++;
|
page++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 分配给系统账户(运营/省/市)
|
// 2. 分配给系统账户(运营/省/市)和待解锁算力 - 在单个事务中执行
|
||||||
const systemAccounts = await this.systemMiningAccountRepository.findAll();
|
const systemAndPendingResult = await this.distributeToSystemAndPending(
|
||||||
for (const systemAccount of systemAccounts) {
|
|
||||||
// 总部账户不直接参与挖矿,它只接收未解锁算力的收益
|
|
||||||
if (systemAccount.accountType === SystemAccountType.HEADQUARTERS) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (systemAccount.totalContribution.isZero()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reward = this.calculator.calculateUserMiningReward(
|
|
||||||
systemAccount.totalContribution,
|
|
||||||
networkTotalContribution,
|
|
||||||
secondDistribution,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!reward.isZero()) {
|
|
||||||
await this.systemMiningAccountRepository.mine(
|
|
||||||
systemAccount.accountType,
|
|
||||||
reward,
|
|
||||||
`秒挖矿 ${currentSecond.getTime()}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 累积系统账户每分钟的挖矿数据到Redis
|
|
||||||
await this.accumulateSystemMinuteData(
|
|
||||||
systemAccount.accountType,
|
|
||||||
currentMinute,
|
|
||||||
reward,
|
|
||||||
systemAccount.totalContribution,
|
|
||||||
networkTotalContribution,
|
|
||||||
secondDistribution,
|
|
||||||
);
|
|
||||||
|
|
||||||
totalDistributed = totalDistributed.add(reward);
|
|
||||||
systemParticipantCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 分配未解锁算力的收益给总部账户
|
|
||||||
const pendingMiningResult = await this.distributePendingContributions(
|
|
||||||
currentSecond,
|
currentSecond,
|
||||||
currentMinute,
|
currentMinute,
|
||||||
networkTotalContribution,
|
networkTotalContribution,
|
||||||
secondDistribution,
|
secondDistribution,
|
||||||
);
|
);
|
||||||
totalDistributed = totalDistributed.add(pendingMiningResult.totalDistributed);
|
totalDistributed = totalDistributed.add(systemAndPendingResult.systemDistributed);
|
||||||
pendingParticipantCount = pendingMiningResult.participantCount;
|
totalDistributed = totalDistributed.add(systemAndPendingResult.pendingDistributed);
|
||||||
|
systemParticipantCount = systemAndPendingResult.systemParticipantCount;
|
||||||
|
pendingParticipantCount = systemAndPendingResult.pendingParticipantCount;
|
||||||
|
|
||||||
// 每分钟结束时,写入汇总的MiningRecord
|
// 每分钟结束时,写入汇总的MiningRecord
|
||||||
if (isMinuteEnd) {
|
if (isMinuteEnd) {
|
||||||
|
|
@ -286,22 +248,72 @@ export class MiningDistributionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分配未解锁算力的挖矿收益给总部账户
|
* 在单个事务中分配系统账户和待解锁算力的挖矿收益
|
||||||
|
* 解决并发事务更新同一行导致的行锁竞争问题
|
||||||
*/
|
*/
|
||||||
private async distributePendingContributions(
|
private async distributeToSystemAndPending(
|
||||||
currentSecond: Date,
|
currentSecond: Date,
|
||||||
currentMinute: Date,
|
currentMinute: Date,
|
||||||
networkTotalContribution: ShareAmount,
|
networkTotalContribution: ShareAmount,
|
||||||
secondDistribution: ShareAmount,
|
secondDistribution: ShareAmount,
|
||||||
): Promise<{ totalDistributed: ShareAmount; participantCount: number }> {
|
): Promise<{
|
||||||
let totalDistributed = ShareAmount.zero();
|
systemDistributed: ShareAmount;
|
||||||
let participantCount = 0;
|
pendingDistributed: ShareAmount;
|
||||||
|
systemParticipantCount: number;
|
||||||
|
pendingParticipantCount: number;
|
||||||
|
}> {
|
||||||
|
let systemDistributed = ShareAmount.zero();
|
||||||
|
let pendingDistributed = ShareAmount.zero();
|
||||||
|
let systemParticipantCount = 0;
|
||||||
|
let pendingParticipantCount = 0;
|
||||||
|
|
||||||
// 获取所有未过期的待解锁算力
|
// 预先获取所有需要的数据
|
||||||
|
const systemAccounts = await this.systemMiningAccountRepository.findAll();
|
||||||
const pendingContributions = await this.prisma.pendingContributionMining.findMany({
|
const pendingContributions = await this.prisma.pendingContributionMining.findMany({
|
||||||
where: { isExpired: false },
|
where: { isExpired: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 计算所有系统账户的挖矿奖励
|
||||||
|
const systemRewards: Array<{
|
||||||
|
accountType: SystemAccountType;
|
||||||
|
reward: ShareAmount;
|
||||||
|
contribution: ShareAmount;
|
||||||
|
memo: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
for (const systemAccount of systemAccounts) {
|
||||||
|
// 总部账户不直接参与挖矿,它只接收未解锁算力的收益
|
||||||
|
if (systemAccount.accountType === SystemAccountType.HEADQUARTERS) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (systemAccount.totalContribution.isZero()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reward = this.calculator.calculateUserMiningReward(
|
||||||
|
systemAccount.totalContribution,
|
||||||
|
networkTotalContribution,
|
||||||
|
secondDistribution,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!reward.isZero()) {
|
||||||
|
systemRewards.push({
|
||||||
|
accountType: systemAccount.accountType,
|
||||||
|
reward,
|
||||||
|
contribution: systemAccount.totalContribution,
|
||||||
|
memo: `秒挖矿 ${currentSecond.getTime()}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算所有待解锁算力的挖矿奖励(归总部账户)
|
||||||
|
const pendingRewards: Array<{
|
||||||
|
pending: any;
|
||||||
|
reward: ShareAmount;
|
||||||
|
memo: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
for (const pending of pendingContributions) {
|
for (const pending of pendingContributions) {
|
||||||
const contribution = new ShareAmount(pending.amount);
|
const contribution = new ShareAmount(pending.amount);
|
||||||
if (contribution.isZero()) continue;
|
if (contribution.isZero()) continue;
|
||||||
|
|
@ -313,29 +325,75 @@ export class MiningDistributionService {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!reward.isZero()) {
|
if (!reward.isZero()) {
|
||||||
// 收益归总部账户
|
pendingRewards.push({
|
||||||
await this.systemMiningAccountRepository.mine(
|
|
||||||
SystemAccountType.HEADQUARTERS,
|
|
||||||
reward,
|
|
||||||
`未解锁算力挖矿 - 来源:${pending.sourceAccountSequence} 类型:${pending.contributionType} 应归:${pending.wouldBeAccountSequence || '无'}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 累积待解锁算力每分钟的挖矿数据到Redis
|
|
||||||
await this.accumulatePendingMinuteData(
|
|
||||||
pending.id,
|
|
||||||
currentMinute,
|
|
||||||
reward,
|
|
||||||
pending,
|
pending,
|
||||||
networkTotalContribution,
|
reward,
|
||||||
secondDistribution,
|
memo: `未解锁算力挖矿 - 来源:${pending.sourceAccountSequence} 类型:${pending.contributionType} 应归:${pending.wouldBeAccountSequence || '无'}`,
|
||||||
);
|
});
|
||||||
|
|
||||||
totalDistributed = totalDistributed.add(reward);
|
|
||||||
participantCount++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { totalDistributed, participantCount };
|
// 如果没有需要处理的数据,直接返回
|
||||||
|
if (systemRewards.length === 0 && pendingRewards.length === 0) {
|
||||||
|
return { systemDistributed, pendingDistributed, systemParticipantCount, pendingParticipantCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在单个事务中执行所有系统账户和待解锁算力的挖矿
|
||||||
|
await this.prisma.$transaction(async (tx) => {
|
||||||
|
// 处理系统账户挖矿
|
||||||
|
for (const { accountType, reward, memo } of systemRewards) {
|
||||||
|
await this.systemMiningAccountRepository.mine(accountType, reward, memo, tx);
|
||||||
|
systemDistributed = systemDistributed.add(reward);
|
||||||
|
systemParticipantCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理待解锁算力挖矿(归总部账户)
|
||||||
|
// 计算总部账户的总收益
|
||||||
|
let headquartersTotal = ShareAmount.zero();
|
||||||
|
const headquartersMemos: string[] = [];
|
||||||
|
|
||||||
|
for (const { reward, memo } of pendingRewards) {
|
||||||
|
headquartersTotal = headquartersTotal.add(reward);
|
||||||
|
headquartersMemos.push(memo);
|
||||||
|
pendingDistributed = pendingDistributed.add(reward);
|
||||||
|
pendingParticipantCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 一次性更新总部账户(而不是每个待解锁算力单独更新)
|
||||||
|
if (!headquartersTotal.isZero()) {
|
||||||
|
await this.systemMiningAccountRepository.mine(
|
||||||
|
SystemAccountType.HEADQUARTERS,
|
||||||
|
headquartersTotal,
|
||||||
|
`秒挖矿 ${currentSecond.getTime()} - 待解锁算力汇总 (${pendingRewards.length}笔)`,
|
||||||
|
tx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 事务成功后,累积 Redis 数据(Redis 操作不需要在事务内)
|
||||||
|
for (const { accountType, reward, contribution } of systemRewards) {
|
||||||
|
await this.accumulateSystemMinuteData(
|
||||||
|
accountType,
|
||||||
|
currentMinute,
|
||||||
|
reward,
|
||||||
|
contribution,
|
||||||
|
networkTotalContribution,
|
||||||
|
secondDistribution,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { pending, reward } of pendingRewards) {
|
||||||
|
await this.accumulatePendingMinuteData(
|
||||||
|
pending.id,
|
||||||
|
currentMinute,
|
||||||
|
reward,
|
||||||
|
pending,
|
||||||
|
networkTotalContribution,
|
||||||
|
secondDistribution,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { systemDistributed, pendingDistributed, systemParticipantCount, pendingParticipantCount };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { ShareAmount } from '../../../domain/value-objects/share-amount.vo';
|
import { ShareAmount } from '../../../domain/value-objects/share-amount.vo';
|
||||||
import { SystemAccountType } from '@prisma/client';
|
import { SystemAccountType, Prisma } from '@prisma/client';
|
||||||
|
|
||||||
|
// 事务客户端类型
|
||||||
|
type TransactionClient = Prisma.TransactionClient;
|
||||||
|
|
||||||
export interface SystemMiningAccountSnapshot {
|
export interface SystemMiningAccountSnapshot {
|
||||||
accountType: SystemAccountType;
|
accountType: SystemAccountType;
|
||||||
|
|
@ -86,13 +89,21 @@ export class SystemMiningAccountRepository {
|
||||||
return new ShareAmount(result._sum.totalContribution || 0);
|
return new ShareAmount(result._sum.totalContribution || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行系统账户挖矿(带外部事务支持)
|
||||||
|
* @param accountType 账户类型
|
||||||
|
* @param amount 挖矿数量
|
||||||
|
* @param memo 备注
|
||||||
|
* @param tx 可选的外部事务客户端,如果不传则自动创建事务
|
||||||
|
*/
|
||||||
async mine(
|
async mine(
|
||||||
accountType: SystemAccountType,
|
accountType: SystemAccountType,
|
||||||
amount: ShareAmount,
|
amount: ShareAmount,
|
||||||
memo: string,
|
memo: string,
|
||||||
|
tx?: TransactionClient,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.prisma.$transaction(async (tx) => {
|
const executeInTx = async (client: TransactionClient) => {
|
||||||
const account = await tx.systemMiningAccount.findUnique({
|
const account = await client.systemMiningAccount.findUnique({
|
||||||
where: { accountType },
|
where: { accountType },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -104,7 +115,7 @@ export class SystemMiningAccountRepository {
|
||||||
const balanceAfter = balanceBefore.plus(amount.value);
|
const balanceAfter = balanceBefore.plus(amount.value);
|
||||||
const totalMined = account.totalMined.plus(amount.value);
|
const totalMined = account.totalMined.plus(amount.value);
|
||||||
|
|
||||||
await tx.systemMiningAccount.update({
|
await client.systemMiningAccount.update({
|
||||||
where: { accountType },
|
where: { accountType },
|
||||||
data: {
|
data: {
|
||||||
totalMined,
|
totalMined,
|
||||||
|
|
@ -112,7 +123,7 @@ export class SystemMiningAccountRepository {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.systemMiningTransaction.create({
|
await client.systemMiningTransaction.create({
|
||||||
data: {
|
data: {
|
||||||
accountType,
|
accountType,
|
||||||
type: 'MINE',
|
type: 'MINE',
|
||||||
|
|
@ -122,7 +133,15 @@ export class SystemMiningAccountRepository {
|
||||||
memo,
|
memo,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (tx) {
|
||||||
|
// 使用外部事务
|
||||||
|
await executeInTx(tx);
|
||||||
|
} else {
|
||||||
|
// 自动创建事务(向后兼容)
|
||||||
|
await this.prisma.$transaction(executeInTx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveMinuteRecord(
|
async saveMinuteRecord(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue