【问题标题】:C# method to lock SQL Server table锁定 SQL Server 表的 C# 方法
【发布时间】:2014-02-11 21:02:42
【问题描述】:

我有一个 C# 程序需要对 SQL Server 表执行一组大规模更新 (20k+)。由于其他用户可以通过内网网站一次更新这些记录,因此我们需要构建具有锁定表功能的 C# 程序。一旦表格被锁定以防止其他用户进行任何更改/搜索,我们将需要执行请求的更新/插入。

由于我们要处理这么多记录,我们不能使用TransactionScope(起初似乎是最简单的方法),因为我们的事务最终由MSDTC service 处理。我们需要使用另一种方法。

根据我在互联网上阅读的内容,使用SqlTransaction 对象似乎是最好的方法,但是我无法锁定表。当程序运行并逐步执行下面的代码时,我仍然能够通过 Intranet 站点执行更新和搜索。

我的问题是双重的。我是否正确使用了SqlTransaction?如果是这样(或者即使不是),是否有更好的方法来获取允许当前运行的程序搜索和执行更新的表锁?

我希望在程序执行下面的代码时锁定表格。

C#

SqlConnection dbConnection = new SqlConnection(dbConn);

dbConnection.Open();

using (SqlTransaction transaction = dbConnection.BeginTransaction(IsolationLevel.Serializable))
{
    //Instantiate validation object with zip and channel values
    _allRecords = GetRecords();
    validation = new Validation();
    validation.SetLists(_allRecords);

    while (_reader.Read())
    {
        try
        {
            record = new ZipCodeTerritory();
            _errorMsg = string.Empty;

            //Convert row to ZipCodeTerritory type
            record.ChannelCode = _reader[0].ToString();
            record.DrmTerrDesc = _reader[1].ToString();
            record.IndDistrnId = _reader[2].ToString();
            record.StateCode = _reader[3].ToString().Trim();
            record.ZipCode = _reader[4].ToString().Trim();
            record.LastUpdateId = _reader[7].ToString();
            record.ErrorCodes = _reader[8].ToString();
            record.Status = _reader[9].ToString();
            record.LastUpdateDate = DateTime.Now;

            //Handle DateTime types separetly
            DateTime value = new DateTime();
            if (DateTime.TryParse(_reader[5].ToString(), out value))
            {
                record.EndDate = Convert.ToDateTime(_reader[5].ToString());
            }
            else
            {
                _errorMsg += "Invalid End Date; ";
            }
            if (DateTime.TryParse(_reader[6].ToString(), out value))
            {
                record.EffectiveDate = Convert.ToDateTime(_reader[6].ToString());
            }
            else
            {
                _errorMsg += "Invalid Effective Date; ";
            }

            //Do not process if we're missing LastUpdateId
            if (string.IsNullOrEmpty(record.LastUpdateId))
            {
                _errorMsg += "Missing last update Id; ";
            }

            //Make sure primary key is valid
            if (_reader[10] != DBNull.Value)
            {
                int id = 0;
                if (int.TryParse(_reader[10].ToString(), out id))
                {
                    record.Id = id;
                }
                else
                {
                    _errorMsg += "Invalid Id; ";
                }
            }

            //Validate business rules if data is properly formatted
            if (string.IsNullOrWhiteSpace(_errorMsg))
            {
                _errorMsg = validation.ValidateZipCode(record);
            }

            //Skip record if any errors found
            if (!string.IsNullOrWhiteSpace(_errorMsg))
            {
                _issues++;

                //Convert to ZipCodeError type in case we have data/formatting errors
                _errors.Add(new ZipCodeError(_reader), _errorMsg);
                continue;
            }
            else if (flag)
            {
                //Separate updates to appropriate list
                SendToUpdates(record);
            }
        }
        catch (Exception ex)
        {
            _errors.Add(new ZipCodeError(_reader), "Job crashed reading this record, please review all columns.");
            _issues++;
        }
    }//End while


    //Updates occur in one of three methods below. If I step through the code,
    //and stop the program here, before I enter any of the methods, and then 
    //make updates to the same records via our intranet site the changes
    //made on the site go through. No table locking has occured at this point. 
    if (flag)
    {
        if (_insertList.Count > 0)
        {
            Updates.Insert(_insertList, _errors);
        }
        if (_updateList.Count > 0)
        {
            _updates = Updates.Update(_updateList, _errors);
            _issues += _updateList.Count - _updates;
        }
        if (_autotermList.Count > 0)
        {
            //_autotermed = Updates.Update(_autotermList, _errors);
            _autotermed = Updates.UpdateWithReporting(_autotermList, _errors);
            _issues += _autotermList.Count - _autotermed;
        }
    } 

    transaction.Commit();
}

【问题讨论】:

  • Updates.Insert 和 Updates.Update 有什么作用?您可能需要发布该代码。他们是否使用不同的连接?在任何一种情况下,您都在执行 BeginTransaction,但没有将该事务设置为执行更新的 Command 对象。此外,您永远不会调用 Transaction.Commit();要检查这一点,在代码运行循环时,转到 SSMS 并选择 @@trancount。如果为 0,则不使用任何事务。
  • 正如大卫所说,事务没有分配给任何东西。它什么也没做。为什么需要锁定整个表进行 20+ K 更新?根据发布的代码,这些更新是独立的。
  • 我们需要锁定表,因为我们作为一个整体单独进行验证和更新/插入。我们不希望验证记录,然后用户使用 Intranet 站点进行更改,这将导致我们刚刚验证的记录突然违反约束或其他内容。
  • 所以可以锁定整个表,验证每一行,然后解锁表,然后让用户进行违反约束的更改?
  • 不,这是我们要避免的。我们想在表被锁定时验证并执行更新/插入。

标签: c# sql .net sql-server locking


【解决方案1】:

SQL 并没有真正提供以独占方式锁定表的方法:它旨在尝试在保持 ACID 的同时最大限度地提高并发使用率。

您可以尝试在查询中使用这些表格提示:

  • 选项卡

    指定在表级别应用获取的锁。锁的类型 获取取决于正在执行的语句。例如,一个 SELECT 语句 可以获取共享锁。通过指定 TABLOCK,共享锁被应用到 整个表而不是行或页级别。如果还指定了 HOLDLOCK,则 表锁一直保持到事务结束。

  • TABLOCKX

    指定对表进行排他锁。

  • 上锁

    指定在事务完成之前获取并持有更新锁。 UPDLOCK 仅在行级或页级为读取操作获取更新锁。如果 UPDLOCK 与 TABLOCK 结合使用,或者为其他一些获取表级锁 原因,将取而代之的是排他 (X) 锁。

  • XLOCK

    指定排他锁将被占用并保持到事务处理 完成。如果使用 ROWLOCK、PAGLOCK 或 TABLOCK 指定,则应用排他锁 到适当的粒度级别。

  • HOLDLOCK/可序列化

    通过在事务完成之前持有共享锁来使共享锁更具限制性, 而不是在所需的表或数据页不存在时立即释放共享锁 不再需要,事务是否已完成。扫描是 以与在 SERIALIZABLE 上运行的事务相同的语义执行 隔离级别。有关隔离级别的更多信息,请参阅 SET TRANSACTION 隔离级别 (Transact-SQL)。

或者,您可以尝试 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE:

  • 语句无法读取已被其他人修改但尚未提交的数据 交易。

  • 没有其他事务可以修改当前事务已读取的数据 直到当前事务完成。

  • 其他事务不能插入带有键值的新行 当前事务中的任何语句读取的键范围,直到当前 交易完成。

范围锁被放置在与搜索条件匹配的键值范围内 在事务中执行的每条语句。这会阻止其他事务更新 或插入符合条件执行的任何语句的任何行 当前交易。这意味着如果事务中的任何语句是 第二次执行时,它们将读取同一组行。范围锁被持有 直到交易完成。这是最严格的隔离级别 因为它锁定了整个范围的键并持有锁直到事务 完成。由于并发性较低,因此仅在必要时使用此选项。这 选项与在所有 SELECT 语句中的所有表上设置 HOLDLOCK 的效果相同 在交易中。

但几乎可以肯定,锁升级会导致阻塞,并且您的用户将几乎死在水中(根据我的经验)。

所以...

等到您有计划维护窗口。将数据库设置为单用户模式,进行更改并将其重新联机。

【讨论】:

    【解决方案2】:

    试试这个:当您从表中获取记录时(在 GetRecords() 函数中?)使用 TABLOCKX 提示:

        SELECT * FROM Table1 (TABLOCKX)
    

    它将在事务之外对所有其他读取和更新进行排队,直到事务被提交或回滚。

    【讨论】:

    • 即使它会排队 20k+ 更新也不会很漂亮。
    • 忘记了.. 但是锁定整张桌子一点也不漂亮。
    • 这是唯一的项。也许我正在重新发明一个迷你队列系统,不管那是我的事,我都想锁住那张该死的桌子。
    【解决方案3】:

    这里都是关于隔离级别的。将您的事务隔离级别更改为 ReadCommited(没有在 C# 中查找枚举值,但应该很接近)。当您对表执行第一次更新/插入时,SQL 将开始锁定,在您提交或回滚事务之前,没有人能够读取您正在更改/添加的数据,前提是他们没有执行脏读(使用 NoLock在他们的 SQL 上,或者将连接隔离级别设置为读取未提交)。但是要小心,根据您插入/更新数据的方式,您可能会在事务期间锁定整个表,但这会导致超时错误客户端在您的事务打开时尝试从该表中读取。没有看到更新背后的 SQL,但我不知道这是否会在这里发生。

    【讨论】:

    • 有没有办法在更新/插入之前锁定表?由于我们有如此多的记录,因此在我们执行验证(基于一组业务规则)和发生更新之间可能会有近一分钟的时间。我们想锁定进入代码的那一秒并开始验证整个数据集。
    【解决方案4】:

    正如有人指出的那样,这笔交易取出后似乎并没有使用。

    根据我们对应用程序/用途的有限信息,很难判断,但从代码 sn-p 来看,在我看来我们不需要任何锁定。我们从源 X(在本例中为 _reader)获取一些数据,然后将数据插入/更新到目标 Y

    所有验证都是针对源数据进行的,以确保它是正确的,看起来我们没有做出任何决定或关心目标中的内容。

    如果上述情况属实,那么更好的方法是将所有这些数据加载到临时表中(可以是真正的临时表“#”或我们之后销毁的真正表,但是目的是一样的),然后在单个 sql 语句中,我们可以从临时表到我们的目的地进行批量插入/更新。假设 db 架构状态良好,20(甚至 30)千条记录应该几乎立即发生,无需等待维护窗口或长时间锁定用户

    也为了严格回答使用事务的问题,下面是一个关于如何正确使用事务的简单示例,网上应该有很多其他示例和信息

    SqlConnection conn = new SqlConnection();
    SqlCommand cmd1 = new SqlCommand();
    SqlTransaction tran = conn.BeginTransaction();
    
    ...
    cmd1.Transaction = tran;
    ...
    tran.Commit();
    

    【讨论】:

    • 这是一个很好的观点。 OP 似乎假设在插入/更新之前验证数据之前保证数据将完全按照请求插入/更新。由于种种原因,这可能是错误的。验证应该发生在插入/更新发生之后,针对数据库本身,最好在同一事务中进行,以便在验证失败时可以执行回滚。
    猜你喜欢
    • 1970-01-01
    • 2023-04-03
    • 1970-01-01
    • 2022-01-11
    • 1970-01-01
    • 2013-09-23
    • 1970-01-01
    • 2019-07-29
    • 1970-01-01
    相关资源
    最近更新 更多