C#方法锁定SQL Server表

14

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

由于我们正在处理如此多的记录,我们不能使用TransactionScope (起初似乎是最简单的方法),因为我们的事务最后会由MSDTC 服务处理。我们需要使用另一种方法。

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

我的问题有两个方面。我是否正确使用了 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();
}

1
Updates.Insert和Updates.Update是做什么的?您可能需要发布该代码。它们是否使用不同的连接?在任一情况下,您都在执行BeginTransaction,但没有将该事务设置为执行更新的Command对象。此外,您从未调用Transaction.Commit(); 为了检查这一点,在代码运行循环时,转到SSMS并执行select @@trancount。如果为0,则未使用任何事务。 - Dmitriy Khaykin
因此,将整个表格锁定,验证每一行,然后解锁表格,再让用户进行违反约束的更改,这样做就可以了吗? - paparazzo
不,那正是我们想要避免的。我们希望在表被锁定时进行验证和更新/插入操作。 - NealR
阅读问题。在锁定和更新之后,让用户进行违反约束的更改是可以的吗? - paparazzo
是的,一旦锁定和更新完成,他们可以尝试进行更改,这可能会违反约束条件。 - NealR
显示剩余2条评论
4个回答

8

SQL并没有提供一种独占锁定表的方式:它旨在尽可能地实现并发使用,同时保持ACID。

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

  • TABLOCK

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

  • TABLOCKX

    指定对表进行排他锁定。

  • UPDLOCK

    指定要获取并保持更新锁,直到事务完成。UPDLOCK仅在行级或页面级上为读操作获取更新锁。如果将UPDLOCK与TABLOCK或出于其他原因需要获取表级锁定,则将获取排他(X)锁定。

  • XLOCK

    指定要获取并保持排他锁,直到事务完成。如果与ROWLOCK、PAGLOCK或TABLOCK一起指定,则排他锁应用于适当的粒度级别。

  • HOLDLOCK/SERIALIZABLE

    通过将共享锁保持到事务完成而不是在不再需要所需表或数据页时立即释放共享锁,使共享锁更加严格。扫描使用与运行于SERIALIZABLE隔离级别的事务相同的语义。有关隔离级别的详细信息,请参阅SET TRANSACTION ISOLATION LEVEL(Transact-SQL)。

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

  • 其他事务修改但未提交的数据,当前事务无法读取。

  • 其他事务无法修改当前事务已读取的数据,直到当前事务完成。

  • 其他事务无法插入新行,其键值范围包含在当前事务中任何语句所读取的键值范围内,直到当前事务完成。

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

但几乎可以肯定,锁升级会导致阻塞,您的用户将基本上无法操作(根据我的经验)。

所以...

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


4
尝试这个:当你从表中获取记录(在GetRecords()函数中?)时,使用TABLOCKX提示:
    SELECT * FROM Table1 (TABLOCKX)

它将在事务提交或回滚之前,将所有其他读取和更新排队在事务之外。


即使它排队等待20k+的更新,也不会很美观。 - paparazzo
忘了那个... 但是锁定整个表格绝对不好看。 - MBulava
这是唯一有效的方法。也许我正在重新发明一个迷你队列系统,但无论如何,这是我的事情,我想锁定该表。 - dudeNumber4

2

这里主要关注的是隔离级别。将你的事务隔离级别更改为ReadCommited(在C#中没有查找枚举值,但应该接近)。当你执行对表的第一个更新/插入时,SQL将开始锁定,直到你提交或回滚事务之前,没有人能够读取你正在更改/添加的数据,前提是他们没有进行脏读(在他们的SQL上使用NoLock,或者将连接隔离级别设置为Read Uncommited)。但要注意,根据你插入/更新数据的方式,你可能会在整个事务期间锁定整个表,这将导致客户端在尝试从此表中读取数据时出现超时错误,而此时你的事务仍然打开。不过,如果没有看到更新背后的SQL语句,我无法确定是否会发生这种情况。


有没有一种方法可以在更新/插入之前锁定表格?由于我们有如此多的记录,当我们执行基于一组业务规则的验证并进行更新时,可能会接近一分钟。我们希望在进入代码并开始验证整个数据集时立即锁定。 - NealR

1
正如有人指出的那样,这个事务似乎在被取出后没有被使用。
从我们对应用程序/目的的有限信息来看,很难确定,但从代码片段来看,我认为我们不需要任何锁定。我们从源X(在这种情况下是_reader)获取一些数据,然后插入/更新到目标Y中。
所有验证都针对源数据进行,以确保其正确性,似乎我们并不关心目的地中的内容或做出任何决策。
如果以上内容属实,则更好的方法是将所有这些数据加载到临时表中(可以是真正的临时表“#”或我们之后销毁的真实表,但目的相同),然后在单个SQL语句中,我们可以从临时表中进行批量插入/更新到我们的目标中。假设数据库架构良好,20(甚至30)万条记录几乎可以立即完成,而无需等待维护窗口或长时间锁定用户。
此外,为了严格回答关于使用事务的问题,以下是一个简单的示例,展示了如何正确使用事务,网络上应该有大量其他示例和信息。
SqlConnection conn = new SqlConnection();
SqlCommand cmd1 = new SqlCommand();
SqlTransaction tran = conn.BeginTransaction();

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

这是一个非常好的观点。OP似乎假设在插入/更新数据之前验证数据可以保证数据会被按照要求插入/更新,但这可能是错误的,原因有很多。验证应该在插入/更新后对数据库本身进行,最好在同一事务中进行,以便在验证失败时执行回滚。 - Dan Bechard

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接