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

多用户并发下ASP.NET(C#)中SQL Server余额读写更新的最优方案

并发场景下SQL Server余额更新的一致性保障方案

在多用户并发操作余额时,核心要解决的是丢失更新问题——即两个用户同时读取同一余额,各自扣除后写回,导致其中一次扣除操作被覆盖。以下是几种实用的解决方法:

1. 直接使用数据库原子更新(优先推荐)

不要在应用层先读取余额再计算更新,而是把扣除逻辑直接嵌入SQL语句,让数据库引擎保证操作的原子性。数据库会自动对目标行加排他锁,确保同一时间只有一个事务能修改该行。

示例SQL:

UPDATE BalanceTable 
SET CurrentBalance = CurrentBalance - @DeductAmount
WHERE AccountId = @AccountId
-- 可选:添加余额校验,防止扣成负数
AND CurrentBalance >= @DeductAmount

C#代码示例(ADO.NET实现):

using (var conn = new SqlConnection("YourConnectionString"))
{
    conn.Open();
    var cmd = new SqlCommand(@"
        UPDATE BalanceTable 
        SET CurrentBalance = CurrentBalance - @DeductAmount
        WHERE AccountId = @AccountId AND CurrentBalance >= @DeductAmount", conn);
    cmd.Parameters.AddWithValue("@AccountId", accountId);
    cmd.Parameters.AddWithValue("@DeductAmount", deductAmount);
    
    int affectedRows = cmd.ExecuteNonQuery();
    if (affectedRows == 0)
    {
        // 处理余额不足或账户不存在的情况
        throw new InvalidOperationException("余额不足或账户不存在");
    }
}

2. 事务+更新锁(需先读取余额的场景)

如果业务必须先读取余额做判断(比如要返回扣除前的余额给用户),可以在读取时加上UPDLOCK(更新锁)和HOLDLOCK(持有锁直到事务结束),确保读取后其他事务无法修改该行,直到当前事务完成更新。

示例代码:

using (var conn = new SqlConnection("YourConnectionString"))
{
    conn.Open();
    using (var tran = conn.BeginTransaction())
    {
        try
        {
            // 读取余额时加更新锁,锁定该行直到事务结束
            var readCmd = new SqlCommand(@"
                SELECT CurrentBalance 
                FROM BalanceTable WITH (UPDLOCK, HOLDLOCK) 
                WHERE AccountId = @AccountId", conn, tran);
            readCmd.Parameters.AddWithValue("@AccountId", accountId);
            decimal currentBalance = (decimal)readCmd.ExecuteScalar();
            
            if (currentBalance < deductAmount)
            {
                tran.Rollback();
                throw new InvalidOperationException("余额不足");
            }
            
            // 执行余额扣除
            var updateCmd = new SqlCommand(@"
                UPDATE BalanceTable 
                SET CurrentBalance = CurrentBalance - @DeductAmount
                WHERE AccountId = @AccountId", conn, tran);
            updateCmd.Parameters.AddWithValue("@AccountId", accountId);
            updateCmd.Parameters.AddWithValue("@DeductAmount", deductAmount);
            updateCmd.ExecuteNonQuery();
            
            tran.Commit();
        }
        catch
        {
            tran.Rollback();
            throw;
        }
    }
}

3. 乐观并发控制(低冲突场景适用)

如果系统并发冲突率不高,可以用乐观锁:给表加一个版本列(比如RowVersion类型,SQL Server会自动维护),读取时同时获取版本号,更新时校验版本号是否和读取时一致——不一致说明有其他事务修改过,需要重试。

首先给表添加版本列:

ALTER TABLE BalanceTable ADD RowVersion ROWVERSION;

C#代码示例:

public bool DeductBalance(int accountId, decimal deductAmount, int retryCount = 3)
{
    using (var conn = new SqlConnection("YourConnectionString"))
    {
        conn.Open();
        for (int i = 0; i < retryCount; i++)
        {
            // 读取余额和版本号
            var readCmd = new SqlCommand(@"
                SELECT CurrentBalance, RowVersion 
                FROM BalanceTable 
                WHERE AccountId = @AccountId", conn);
            readCmd.Parameters.AddWithValue("@AccountId", accountId);
            using (var reader = readCmd.ExecuteReader())
            {
                if (!reader.Read())
                    return false;
                
                decimal currentBalance = (decimal)reader["CurrentBalance"];
                byte[] originalVersion = (byte[])reader["RowVersion"];
                
                if (currentBalance < deductAmount)
                    return false;
                
                reader.Close();
                
                // 尝试更新,校验版本号
                var updateCmd = new SqlCommand(@"
                    UPDATE BalanceTable 
                    SET CurrentBalance = CurrentBalance - @DeductAmount
                    WHERE AccountId = @AccountId AND RowVersion = @OriginalVersion", conn);
                updateCmd.Parameters.AddWithValue("@AccountId", accountId);
                updateCmd.Parameters.AddWithValue("@DeductAmount", deductAmount);
                updateCmd.Parameters.AddWithValue("@OriginalVersion", originalVersion);
                
                int affectedRows = updateCmd.ExecuteNonQuery();
                if (affectedRows > 0)
                    return true; // 更新成功
                
                // 版本不一致,进入下一次重试
            }
        }
        return false; // 多次重试失败
    }
}

关键注意事项

  • 避免使用READ UNCOMMITTED隔离级别,会导致脏读;优先用REPEATABLE READ或更高隔离级别,配合锁提示。
  • 不要在事务中执行无关操作,尽量缩短事务时长,减少锁持有时间,降低并发冲突概率。
  • 高并发场景下,优先选择数据库原子更新方案,性能和可靠性最优。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 22:32:05