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

C# WinForm多用户同时触发按钮致无限循环失效问题求助

SQL并发控制失效的解决办法

问题描述

在C# WinForm项目中,通过SQL字段(0/1切换)实现任务X的并发控制:当字段为0时执行流程,为1时进入等待循环,防止多人同时处理任务。但多个用户同时触发操作时,所有用户都会在某一用户将字段更新为1前读到0值,导致并发控制失效。

原代码示例:

SqlDataReader VerifyIfBusyReader = null;

SqlCommand VerifyIfBusy = new SqlCommand("SELECT [Value] FROM [Variables] WHERE [TYPE] = 'IsBusy'", CONNECTION);
int verify = 1;
int counts = 0;
int milliseconds = 2000;
while (verify == 1)
{
    counts++;
    if (counts == 30)
    {
        break;
    }

    VerifyIfBusyReader = VerifyIfBusy.ExecuteReader();
    VerifyIfBusyReader.Read();
    if (int.Parse(VerifyIfBusyReader[0].ToString()) == 1)
    {
        VerifyIfBusyReader.Close();
    }
    else if (int.Parse(VerifyIfBusyReader[0].ToString()) == 0)
    {
        verify = 0;
        VerifyIfBusyReader.Close();
    }
    
    Thread.Sleep(milliseconds);
}

SqlCommand IsBusyNow = new SqlCommand("UPDATE [Variables] SET [VALUE] = 1 WHERE [TYPE] = 'IsBusy'", CONNECTION);
VerifyIfBusyReader = IsBusyNow.ExecuteReader();
VerifyIfBusyReader.Close();

注:流程结束后已实现将字段改回0的逻辑,无问题。

问题根源

原代码的查询-更新操作是非原子的:多个用户可以同时执行SELECT读到0值,之后又都执行UPDATE将字段设为1,完全绕过了并发控制逻辑。必须让“检查字段值+更新字段值”成为不可分割的原子操作,才能避免这种竞态条件。

解决办法

1. 原子化UPDATE操作(推荐)

将检查与更新合并为一条SQL语句,利用数据库的行锁机制保证原子性。执行UPDATE时仅在字段为0的情况下修改,然后通过ExecuteNonQuery()返回的影响行数判断是否成功抢到“锁”:

int counts = 0;
int milliseconds = 2000;
bool acquiredLock = false;

while (!acquiredLock && counts < 30)
{
    counts++;
    // 原子操作:仅当Value为0时更新为1
    SqlCommand acquireLockCmd = new SqlCommand(@"
        UPDATE [Variables] 
        SET [VALUE] = 1 
        WHERE [TYPE] = 'IsBusy' AND [Value] = 0", CONNECTION);
    
    int affectedRows = acquireLockCmd.ExecuteNonQuery();
    if (affectedRows > 0)
    {
        // 成功抢到锁,退出循环执行任务
        acquiredLock = true;
    }
    else
    {
        // 未抢到锁,等待后重试
        Thread.Sleep(milliseconds);
    }
}

if (acquiredLock)
{
    // 执行任务X的核心逻辑
}
else
{
    // 超时未抢到锁,提示用户“当前有其他用户在处理,请稍后再试”
}

2. 事务+行级锁

开启数据库事务,查询时对目标行加更新锁,确保其他事务无法读取该行的旧值,再检查并更新字段,保证整个过程的原子性:

bool acquiredLock = false;
int counts = 0;
int milliseconds = 2000;

while (!acquiredLock && counts < 30)
{
    counts++;
    using (SqlTransaction transaction = CONNECTION.BeginTransaction())
    {
        try
        {
            // 查询时加更新锁,阻止其他事务读取该行
            SqlCommand checkCmd = new SqlCommand(@"
                SELECT [Value] 
                FROM [Variables] WITH (UPDLOCK) 
                WHERE [TYPE] = 'IsBusy'", CONNECTION, transaction);
            
            int currentValue = (int)checkCmd.ExecuteScalar();
            if (currentValue == 0)
            {
                // 更新字段为1
                SqlCommand updateCmd = new SqlCommand(@"
                    UPDATE [Variables] 
                    SET [VALUE] = 1 
                    WHERE [TYPE] = 'IsBusy'", CONNECTION, transaction);
                updateCmd.ExecuteNonQuery();
                
                // 提交事务,确认锁生效
                transaction.Commit();
                acquiredLock = true;
            }
            else
            {
                // 释放事务,等待重试
                transaction.Rollback();
                Thread.Sleep(milliseconds);
            }
        }
        catch (Exception ex)
        {
            transaction.Rollback();
            // 记录异常日志,等待后重试
            Thread.Sleep(milliseconds);
        }
    }
}

if (acquiredLock)
{
    // 执行任务X的核心逻辑
}
else
{
    // 超时提示用户
}

关键说明

  • 原子化操作是解决这类并发问题的核心,依赖数据库本身的锁机制保证操作不可分割。
  • 两种方法中,原子UPDATE更简洁高效,推荐优先使用;事务+行锁适合需要更复杂业务判断的场景。
  • 务必在任务执行完成后,将IsBusy字段改回0,避免后续用户无法获取锁。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 18:03:16