简体   繁体   English

C#方法锁定SQL Server表

[英]C# method to lock SQL Server table

I have a C# program that needs to perform a group of mass updates (20k+) to a SQL Server table. 我有一个C#程序需要对SQL Server表执行一组批量更新(20k +)。 Since other users can update these records one at a time via an intranet website, we need to build the C# program with the capability of locking the table down. 由于其他用户可以通过Intranet网站一次更新这些记录,因此我们需要构建能够锁定表的C#程序。 Once the table is locked to prevent another user from making any alterations/searches we will then need to preform the requested updates/inserts. 一旦表被锁定以防止其他用户进行任何更改/搜索,我们将需要执行所请求的更新/插入。

Since we are processing so many records, we cannot use TransactionScope (seemed the easiest way at first) due to the fact our transaction winds up being handled by the MSDTC service . 由于我们正在处理如此多的记录,因此我们不能使用TransactionScope (最初似乎是最简单的方法),因为我们的交易最终由MSDTC服务处理。 We need to use another method. 我们需要使用另一种方法。

Based on what I've read on the internet using a SqlTransaction object seemed to be the best method, however I cannot get the table to lock. 根据我在互联网上阅读的内容,使用SqlTransaction对象似乎是最好的方法,但是我无法让表锁定。 When the program runs and I step through the code below, I'm still able to perform updates and search via the intranet site. 当程序运行并且我单步执行下面的代码时,我仍然可以通过Intranet站点执行更新和搜索。

My question is twofold. 我的问题是双重的。 Am I using the SqlTransaction properly? 我正确使用SqlTransaction吗? If so (or even if not) is there a better method for obtaining a table lock that allows the current program running to search and preform updates? 如果是这样(或者即使没有)有更好的方法来获得一个表锁,允许当前程序运行来搜索和预先形成更新?

I would like for the table to be locked while the program executes the code below. 我希望在程序执行下面的代码时锁定表。

C# 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();
}

SQL doesn't really provide a way to exclusively lock a table: it's designed to try to maximize concurrent use while keeping ACID. SQL并没有真正提供一种独占锁定表的方法:它旨在尝试最大化并发使用,同时保持ACID。

You could try using these table hints on your queries: 您可以尝试在查询中使用这些表提示:

  • TABLOCK TABLOCK

    Specifies that the acquired lock is applied at the table level. 指定在表级别应用获取的锁定。 The type of lock that is acquired depends on the statement being executed. 获取的锁类型取决于正在执行的语句。 For example, a SELECT statement may acquire a shared lock. 例如,SELECT语句可以获取共享锁。 By specifying TABLOCK, the shared lock is applied to the entire table instead of at the row or page level. 通过指定TABLOCK,共享锁将应用于整个表而不是行或页级。 If HOLDLOCK is also specified, the table lock is held until the end of the transaction. 如果还指定了HOLDLOCK,表锁将保持到事务结束。

  • TABLOCKX TABLOCKX

    Specifies that an exclusive lock is taken on the table. 指定对表执行独占锁定。

  • UPDLOCK UPDLOCK

    Specifies that update locks are to be taken and held until the transaction completes. 指定在事务完成之前采用并保持更新锁。 UPDLOCK takes update locks for read operations only at the row-level or page-level. UPDLOCK仅在行级或页级别为读取操作获取更新锁。 If UPDLOCK is combined with TABLOCK, or a table-level lock is taken for some other reason, an exclusive (X) lock will be taken instead. 如果UPDLOCK与TABLOCK结合使用,或者由于其他原因而采用表级锁定,则将采用独占(X)锁定。

  • XLOCK XLOCK

    Specifies that exclusive locks are to be taken and held until the transaction completes. 指定在事务完成之前采用并保持独占锁。 If specified with ROWLOCK, PAGLOCK, or TABLOCK, the exclusive locks apply to the appropriate level of granularity. 如果使用ROWLOCK,PAGLOCK或TABLOCK指定,则排它锁适用于适当的粒度级别。

  • HOLDLOCK/SERIALIZABLE HOLDLOCK / SERIALIZABLE

    Makes shared locks more restrictive by holding them until a transaction is completed, instead of releasing the shared lock as soon as the required table or data page is no longer needed, whether the transaction has been completed or not. 通过保持共享锁更加严格,直到事务完成,而不是在不再需要所需的表或数据页时释放共享锁,无论事务是否已完成。 The scan is performed with the same semantics as a transaction running at the SERIALIZABLE isolation level. 执行扫描的语义与在SERIALIZABLE隔离级别运行的事务相同。 For more information about isolation levels, see SET TRANSACTION ISOLATION LEVEL (Transact-SQL). 有关隔离级别的更多信息,请参阅SET TRANSACTION ISOLATION LEVEL(Transact-SQL)。

Alternatively, you could try SET TRANSACTION ISOLATION LEVEL SERIALIZABLE: 或者,您可以尝试SET TRANSACTION ISOLATION LEVEL SERIALIZABLE:

  • Statements cannot read data that has been modified but not yet committed by other transactions. 语句无法读取已修改但尚未由其他事务提交的数据。

  • No other transactions can modify data that has been read by the current transaction until the current transaction completes. 在当前事务完成之前,没有其他事务可以修改当前事务已读取的数据。

  • Other transactions cannot insert new rows with key values that would fall in the range of keys read by any statements in the current transaction until the current transaction completes. 其他事务无法插入新行,其键值将落在当前事务中任何语句读取的键范围内,直到当前事务完成为止。

Range locks are placed in the range of key values that match the search conditions of each statement executed in a transaction. 范围锁定位于与事务中执行的每个语句的搜索条件匹配的键值范围内。 This blocks other transactions from updating or inserting any rows that would qualify for any of the statements executed by the current transaction. 这会阻止其他事务更新或插入任何符合当前事务执行的任何语句的行。 This means that if any of the statements in a transaction are executed a second time, they will read the same set of rows. 这意味着如果事务中的任何语句第二次执行,它们将读取同一组行。 The range locks are held until the transaction completes. 范围锁保持到事务完成。 This is the most restrictive of the isolation levels because it locks entire ranges of keys and holds the locks until the transaction completes. 这是隔离级别中最具限制性的,因为它会锁定整个键范围并保持锁定直到事务完成。 Because concurrency is lower, use this option only when necessary. 由于并发性较低,因此仅在必要时使用此选项。 This option has the same effect as setting HOLDLOCK on all tables in all SELECT statements in a transaction. 此选项与在事务中所有SELECT语句中的所有表上设置HOLDLOCK具有相同的效果。

But almost certainly, lock escalation will cause blocking and your users will be pretty much dead in the water (in my experience). 但几乎可以肯定的是,锁定升级会导致阻塞,并且您的用户将在水中死亡(根据我的经验)。

So... 所以...

Wait until you have a schedule maintenance window. 等到你有一个计划维护窗口。 Set the database in single-user mode, make your changes and bring it back online. 以单用户模式设置数据库,进行更改并将其重新联机。

Try this: when you get records from you table (in the GetRecords() function?) use TABLOCKX hint: 试试这个:当你从表中获取记录时(在GetRecords()函数中?)使用TABLOCKX提示:

    SELECT * FROM Table1 (TABLOCKX)

It will queue all other reads and updates outside your transaction until the transaction is either commited or rolled back. 它将在事务外部排队所有其他读取和更新,直到事务被提交或回滚。

It's all about Isolation level here. 这就是隔离级别的全部内容。 Change your Transaction Isolation Level to ReadCommited (didn't lookup the Enum Value in C# but that should be close). 将您的事务隔离级别更改为ReadCommited(未在C#中查找枚举值,但应该关闭)。 When you execute the first update/insert to the table, SQL will start locking and no one will be able to read the data you're changing/adding until you Commit or Rollback thr transaction, provided they are not performing dirty reads (using NoLock on their SQL, or have the connection Isolation level set to Read Uncommited).. Be careful though, depending on how you're inserting/updating data you may lock the whole table for the duration of your transaction though which would cause timeout errors at the client when they try to read from this table while your transaction is open. 当您对表执行第一次更新/插入时,SQL将开始锁定,并且没有人能够读取您正在更改/添加的数据,直到您提交或回滚thr事务,前提是它们没有执行脏读(使用NoLock)在他们的SQL上,或将连接隔离级别设置为Read Uncommited)。但要注意,根据您插入/更新数据的方式,您可能会在事务持续期间锁定整个表,但这会导致超时错误客户端在您的交易处于打开状态时尝试从此表中读取数据时。 Without seeing the SQL behind the updates though I can't tell if that will happen here. 没有看到更新背后的SQL虽然我不知道这是否会发生在这里。

As someone has pointed out, the transaction doesn't seem to be used after being taken out. 正如有人指出的那样,交易在被取出后似乎没有被使用。

From the limited information we have on the app/purpose, it's hard to tell, but from the code snippet, it seems to me we don't need any locking. 从我们在应用程序/目的上的有限信息来看,很难说,但从代码片段来看,在我看来,我们不需要任何锁定。 We are getting some data from source X (in this case _reader) and then inserting/updating into destination Y . 我们从源X获取一些数据(在本例中为_reader),然后插入/更新到目标Y.

All the validation happens against the source data to make sure it's correct, it doesn't seem like we're making any decision or care for what's in the destination. 所有的验证都是针对源数据进行的,以确保它是正确的,似乎我们不会做出任何决定或关心目标中的内容。

If the above is true then a better approach would be to load all this data into a temporary table (can be a real temp table "#" or a real table that we destroy afterwards, but the purpose is the same), and then in a single sql statement, we can do a mass insert/update from the temp table into our destination. 如果以上是真的,那么更好的方法是将所有这些数据加载到临时表中(可以是真正的临时表“#”或我们之后销毁的真实表,但目的是相同的),然后在在单个sql语句中,我们可以从临时表中进行批量插入/更新到我们的目标。 Assuming the db schema is in decent shape, 20 (or even 30) thousand records should happen almost instantly without any need to wait for maintenance window or lock out users for extended periods of time 假设数据库模式处于体面状态,20个(甚至30个)千条记录几乎可以立即发生,无需等待维护窗口或长时间锁定用户

Also to strictly answer the question about using transaction, below is a simple sample on how to properly use a transaction, there should be plenty of other samples and info on the web 另外要严格回答有关使用交易的问题,下面是关于如何正确使用交易的简单示例,网上应该有大量其他样本和信息

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

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM