问题 C#方法锁定SQL Server表


我有一个C#程序需要对SQL Server表执行一组批量更新(20k +)。由于其他用户可以通过Intranet网站一次更新这些记录,我们需要构建能够锁定表的C#程序。一旦表被锁定以防止其他用户进行任何更改/搜索,我们将需要预先形成所请求的更新/插入。

由于我们正在处理这么多记录,我们无法使用 TransactionScope (这似乎是最简单的方式)因为我们的交易最终被处理了 MSDTC服务。我们需要使用另一种方法。

基于我在互联网上阅读的内容 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();
}

1596
2018-02-11 21:02


起源

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


答案:


SQL并没有真正提供一种独占锁定表的方法:它旨在尝试在保持ACID的同时最大化并发使用。

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

  • TABLOCK

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

  • TABLOCKX

    指定对表执行独占锁定。

  • UPDLOCK

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

  • XLOCK

    指定在事务之前采用并保持独占锁   完成。如果使用ROWLOCK,PAGLOCK或TABLOCK指定,则应用独占锁   到适当的粒度级别。

  • HOLDLOCK / SERIALIZABLE

    通过保持共享锁更具限制性,直到事务完成,   一旦所需的表或数据页为否,就不再释放共享锁   交易是否已经完成,需要更长时间。扫描是   使用与在SERIALIZABLE运行的事务相同的语义执行   隔离级别。有关隔离级别的更多信息,请参阅SET TRANSACTION   隔离级别(Transact-SQL)。

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

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

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

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

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

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

所以...

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


7
2018-02-11 21:21





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

    SELECT * FROM Table1 (TABLOCKX)

它会将事务外部的所有其他读取和更新排入队列,直到事务被提交或回滚。


3
2018-02-11 21:20



即使它排队20k +更新也不会很漂亮。 - paparazzo
忘了那个..但是你整个桌子的锁定都不会很漂亮。 - MBulava


这就是隔离级别的全部内容。将您的事务隔离级别更改为ReadCommited(未在C#中查找枚举值,但应该关闭)。当您对表执行第一次更新/插入时,SQL将开始锁定,并且没有人能够读取您正在更改/添加的数据,直到您提交或回滚thr事务,前提是它们不执行脏读(使用NoLock)在他们的SQL上,或将连接隔离级别设置为Read Uncommited)。但要注意,根据您插入/更新数据的方式,您可能会在事务持续期间锁定整个表,但这会导致超时错误客户端在您的交易处于打开状态时尝试从此表中读取数据时。没有看到更新背后的SQL虽然我不知道这是否会发生在这里。


2
2018-02-11 21:14



有没有办法在更新/插入之前锁定表?由于我们有很多记录,因此在我们执行验证(基于一组业务规则)和更新发生之间可能会有一分钟的时间。我们想锁定我们进入代码的第二个并开始验证整个数据集。 - NealR


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

从我们在应用程序/目的上的有限信息来看,很难说,但从代码片段来看,在我看来,我们不需要任何锁定。我们从源头获取一些数据 X(在本例中为_reader)然后插入/更新到目标 ÿ

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

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

另外要严格回答有关使用交易的问题,下面是关于如何正确使用交易的简单示例,网上应该有大量其他样本和信息

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

...
cmd1.Transaction = tran;
...
tran.Commit();

1
2018-02-11 21:47



这是一个很好的观点。 OP似乎在假设验证数据 之前 插入/更新可确保按要求完全插入/更新数据。由于种种原因,这可能是错误的。验证应该发生 后 对数据库本身进行了插入/更新,最好是在同一事务中,以便在验证失败时可以执行回滚。 - Dan Bechard