You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET Core异步API并发问题:加密货币提现超余额漏洞求助

解决异步场景下加密货币提现的余额竞态问题

问题根源

你遇到的是典型的竞态条件:多个异步请求同时读取用户余额,都通过校验后执行转账,最终导致余额透支。进程内的lock或信号量无效有两个核心原因:

  1. lock是线程锁,异步方法中await会释放当前线程,后续代码可能在其他线程执行,锁无法跨线程生效;
  2. 若项目是多实例部署,进程内锁无法覆盖不同实例的请求,完全起不到全局互斥的作用。

解决方案

方案1:数据库原子校验+扣减(最推荐)

放弃「先查余额再判断」的分离逻辑,直接用数据库的原子操作完成校验与扣减。数据库的UPDATE语句是原子执行的,同一时间只有一个请求能满足条件并修改余额,从根源避免竞态。

示例代码:

public async Task PerformAtomicWithdraw(decimal withdrawAmount, Guid customerId)
{
    // 执行原子更新:仅当余额充足时扣减,返回受影响行数
    var affectedRows = await _dbContext.Database.ExecuteSqlInterpolatedAsync(
        $"UPDATE CustomerBalances SET Balance = Balance - {withdrawAmount} WHERE CustomerId = {customerId} AND Balance >= {withdrawAmount}"
    );

    // 受影响行数为0,说明余额不足
    if (affectedRows == 0)
    {
        throw new Exception("Insufficient balance");
    }

    // 这里执行后续转账操作(比如链上交易处理)
    await _transactionService.ProcessBlockchainTransfer(customerId, withdrawAmount);
}

优势:无需额外锁机制,性能最优,天然支持分布式部署,完全避免竞态。

方案2:分布式锁(多实例场景)

如果必须保留「先校验再操作」的逻辑,需使用分布式锁(比如基于Redis、ZooKeeper实现),确保同一时间只有一个请求能处理该用户的提现流程。

示例伪代码(基于Redis分布式锁):

private readonly IDistributedLock _redisDistributedLock;

public async Task ProcessWithdrawRequest(decimal withdrawAmount, Guid customerId)
{
    // 尝试获取针对该用户的分布式锁,有效期10秒(需覆盖整个操作流程)
    using (var lockHandle = await _redisDistributedLock.TryAcquireLockAsync($"withdraw-lock:{customerId}", TimeSpan.FromSeconds(10)))
    {
        if (lockHandle == null)
        {
            throw new Exception("Too many concurrent requests, please try again later");
        }

        // 锁内执行校验与转账
        var currentBalance = await _transactionService.GetCustomerBalance(customerId);
        if (currentBalance < withdrawAmount)
            throw new Exception("Insufficient balance");

        await _transactionService.PerformWithdraw(customerId, withdrawAmount);
    }
}

注意事项:锁的有效期要设置足够覆盖校验+转账的全流程,同时要处理锁获取失败的场景(返回繁忙提示),避免死锁。

方案3:数据库行锁(单数据库实例场景)

如果是单数据库实例部署,可在查询余额时使用行锁(SELECT ... FOR UPDATE),锁住该用户的余额记录,直到事务提交,确保校验与修改的原子性。

示例代码:

public async Task ProcessWithdrawWithRowLock(decimal withdrawAmount, Guid customerId)
{
    using (var transaction = await _dbContext.Database.BeginTransactionAsync())
    {
        try
        {
            // 查询时加行锁,其他请求需等待当前事务完成才能读取该记录
            var customerBalance = await _dbContext.CustomerBalances
                .FromSqlInterpolated($"SELECT * FROM CustomerBalances WHERE CustomerId = {customerId} FOR UPDATE")
                .FirstOrDefaultAsync();

            if (customerBalance == null || customerBalance.Balance < withdrawAmount)
                throw new Exception("Insufficient balance");

            // 扣减余额
            customerBalance.Balance -= withdrawAmount;
            await _dbContext.SaveChangesAsync();

            // 执行转账操作
            await _transactionService.PerformWithdraw(customerId, withdrawAmount);

            await transaction.CommitAsync();
        }
        catch
        {
            await transaction.RollbackAsync();
            throw;
        }
    }
}

优势:无需引入外部组件,依赖数据库自身的锁机制,但仅适用于单数据库实例场景。

总结

优先选择方案1,原子操作是处理这类资金操作竞态最可靠、高效的方式。若因业务限制必须保留原有逻辑,再根据部署场景选择分布式锁或数据库行锁。

内容的提问来源于stack exchange,提问作者Moradof

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 17:31:14