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

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具有相同的效果。

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

所以…

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

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

  SELECT * FROM Table1 (TABLOCKX) 

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

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

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

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

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

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

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

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