如何避免并发冲突!

某些应用程序在更新数据库数据时采用“后进有效”(Last in Wins) 法。使用“后进有效”法更新数据库时不会将更新与原始记录相比较,因此可能会覆盖掉自上次刷新记录以来其他用户所做的所有更改。然而,有时应用程序却需要在执行更新之前确定数据自最初读取以来是否被更改。 数据访问逻辑组件可以实现管理锁定和并发的代码。管理锁定和并发的方法有两种: • 保守式并发。为进行更新而读取某行数据的用户可以在数据源中对该行设置一个锁定。在该用户解除锁定之前,其他任何用户都不能更改该行。 • 开放式并发。用户在读取某行数据时不锁定该行。其他用户可以在同一时间自由访问该行。当用户要更新某行数据时,应用程序必须确定自该行被读取以来其他用户是否进行过更改。尝试更新已经过更改的记录会导致并发冲突。 使用保守式并发 保守式并发主要用于数据争用量大以及通过锁定来保护数据的成本低于发生并发冲突时回滚事务的成本的环境。如果锁定时间很短(例如在编程处理的记录中),则实现保守式并发效果最好。 保守式并发要求与数据库建立持久连接,并且因为记录可能被锁定较长时间,因此当用户与数据进行交互时,不能提供可缩放的性能。 使用开放式并发 开放式并发适用于数据争用量低或要求只读访问数据的环境。开放式并发可以减少所需锁定的数量,从而降低数据库服务器的负荷,提高数据库的性能。 开放式并发在 .NET 中被广泛使用以满足移动和脱机应用程序的需要。在这种情况下,长时间锁定数据是不可行的。此外,保持记录锁定还要求与数据库服务器的持久连接,这在脱机应用程序中是不可能的。 测试开放式并发冲突 测试开放式并发冲突的方法有多种: • 使用分布式时间戳。分布式时间戳适用于不要求协调的环境。在数据库的每个表中添加一个时间戳列或版本列。时间戳列与对表内容的查询一起返回。当试图更新时,数据库中的时间戳值将与被修改行中的原始时间戳值进行比较。如果这两个值匹配,则执行更新,同时时间戳列被更新为当前时间以反映更新。如果这两个值不匹配,则发生开放式并发冲突。 • 保留原始数据值的副本。在查询数据库的数据时保留原始数据值的一个副本。在更新数据库时,检查数据库的当前值是否与原始值匹配。 • 原始值保存在 DataSet 中,当更新数据库时,数据适配器可以使用该原始值执行开放式并发检查。 • 使用集中的时间戳。在数据库中定义一个集中的时间戳表,用于记录对任何表中的任何行的更新。例如,时间戳表可以显示以下信息:“2002 年 3 月 26 日下午 2:56 约翰更新了表 XYZ 中的行 1234”。 集中的时间戳适用于签出方案以及某些 脱机客户端方案,其中可能需要明确的锁定所有者和替代管理。此外,集中的时间戳还可以根据需要提供审核。 手动实现开放式并发 请考虑以下 SQL 查询: SELECT Column1, Column2, Column3 FROM Table1 要在更新 Table1 的行时测试开放式并发冲突,可以发出以下 UPDATE 语句: UPDATE Table1 Set Column1 = @NewValueColumn1, Set Column2 = @NewValueColumn2, Set Column3 = @NewValueColumn3 WHERE Column1 = @OldValueColumn1 AND Column2 = @OldValueColumn2 AND Column3 = @OldValueColumn3 如果原始值与数据库中的值匹配,则执行更新。如果某个值被修改,WHERE 子句将无法找到相应匹配,从而更新将不会修改该行。您可以对此技术稍加变化,即只对特定列应用 WHERE 子句,使得如果自上次查询以来特定字段被更新,则不覆盖数据。 注意:请始终返回一个唯一标识查询中的一行的值,例如一个主关键字,以用于 UPDATE 语句的 WHERE 子句。这样可以确保 UPDATE 语句更新正确的行。 如果数据源中的列允许空值,则可能需要扩展 WHERE 子句,以便检查本地表与数据源中匹配的空引用。例如,以下 UPDATE 语句将验证本地行中的空引用(或值)是否仍然与数据源中的空引用(或值)相匹配。 UPDATE Table1 Set Column1 = @NewColumn1Value WHERE (@OldColumn1Value IS NULL AND Column1 IS NULL) OR Column1 = @OldColumn1Value 使用数据适配器和 DataSet 实现开放式并发 : 可以配合使用 DataAdapter.RowUpdated 事件与前面所述技术以通知您的应用程序发生了开放式并发冲突。每当试图更新 DataSet 中的修改过的行时,都将引发 RowUpdated 事件。可以使用 RowUpdated 事件添加特殊处理代码,包括发生异常时的处理、添加自定义错误信息以及添加重试逻辑。 RowUpdated 事件处理程序接收一个 RowUpdatedEventArgs 对象,该对象具有 RecordsAffected 属性,可以显示针对表中的一个修改过的行的更新命令会影响多少行。如果把更新命令设置为测试开放式并发,则当发生开放式并发冲突时,RecordsAffected 属性将为 0。设置 RowUpdatedEventArgs.Status 属性以表明要采取的操作;例如,可以把该属性设置为 UpdateStatus.SkipCurrentRow 以跳过对当前行的更新,但是继续更新该更新命令中的其他行。 使用数据适配器测试并发错误的另一种方法是在调用 Update 方法之前把 DataAdapter.ContinueUpdateOnError 属性设置为 true。完成更新后,调用 DataTable 对象的 GetErrors 方法以确定哪些行发生了错误。然后,使用这些行的 RowError 属性找到特定的详细错误信息。 以下代码示例显示了 Customer 数据访问逻辑组件如何检查并发冲突。该示例假设客户端检索到了一个 DataSet 并修改了数据,然后把该 DataSet 传递给了数据访问逻辑组件中的 UpdateCustomer 方法。UpdateCustomer 方法将通过调用以下存储过程来更新相应的客户记录;仅当客户 ID 与公司名称未被修改时存储过程才能更新该客户记录: CREATE PROCEDURE CustomerUpdate { @CompanyName varchar(30), @oldCustomerID varchar(10), @oldCompanyName varchar(30) } AS UPDATE Customers Set CompanyName = @CompanyName WHERE CustomerID = @oldCustomerID AND CompanyName = @oldCompanyName GO 在 UpdateCustomer 方法中,以下代码示例将一个数据适配器的 UpdateCommand 属性设置为测试开放式并发,然后使用 RowUpdated 事件测试开放式并发冲突。如果遇到开放式并发冲突,应用程序将通过设置要更新的行的 RowError 来表明开放式并发冲突。注意,传递给 UPDATE 命令中的 WHERE 子句的参数值被映射到 DataSet 中各相应列的原始值。 // CustomerDALC 类中的 UpdateCustomer 方法 public void UpdateCustomer(DataSet dsCustomer) { // 连接到 Northwind 数据库 SqlConnection cnNorthwind = new SqlConnection( "Data source=localhost;Integrated security=SSPI;Initial Catalog=northwind"); // 创建一个数据适配器以访问 Northwind 中的 Customers 表 SqlDataAdapter da = new SqlDataAdapter(); // 设置数据适配器的 UPDATE 命令,调用存储过程“UpdateCustomer” da.UpdateCommand = new SqlCommand("CustomerUpdate", cnNorthwind); da.UpdateCommand.CommandType = CommandType.StoredProcedure; // 向数据适配器的 UPDATE 命令添加两个参数, // 为 WHERE 子句指定信息(用于检查开放式并发冲突) da.UpdateCommand.Parameters.Add("@CompanyName", SqlDbType.NVarChar, 30, "CompanyName"); // 将 CustomerID 的原始值指定为第一个 WHERE 子句参数 SqlParameter myParm = da.UpdateCommand.Parameters.Add( "@oldCustomerID", SqlDbType.NChar, 5, "CustomerID"); myParm.SourceVersion = DataRowVersion.Original; // 将 CustomerName 的原始值指定为第二个 WHERE 子句参数 myParm = da.UpdateCommand.Parameters.Add( "@oldCompanyName", SqlDbType.NVarChar, 30, "CompanyName"); myParm.SourceVersion = DataRowVersion.Original; // 为 RowUpdated 事件添加一个处理程序 da.RowUpdated += new SqlRowUpdatedEventHandler(OnRowUpdated); // 更新数据库 da.Update(ds, "Customers"); foreach (DataRow myRow in ds.Tables["Customers"].Rows) { if (myRow.HasErrors) Console.WriteLine(myRow[0] + " " + myRow.RowError); } } // 处理 RowUpdated 事件的方法。 如果登记该事件但不处理它, // 则引发一个 SQL 异常。 protected static void OnRowUpdated(object sender, SqlRowUpdatedEventArgs args) { if (args.RecordsAffected == 0) { args.Row.RowError = "遇到开放式并发冲突"; args.Status = UpdateStatus.SkipCurrentRow; } } 当在一个 SQL Server 存储过程中执行多个 SQL 语句时,出于性能原因,可以使用 SET NOCOUNT ON 选项。此选项将禁止 SQL Server 在每次执行完一条语句时都向客户端返回一条消息,从而可以降低网络流量。然而,这样将不能像前面的代码示例那样检查 RecordsAffected 属性。RecordsAffected 属性将始终为 1。另一种方法是在存储过程中返回 @@ROWCOUNT 函数(或将它指定为一个输出参数);@@ROWCOUNT 包含了存储过程中上一条语句完成时的记录数目,并且即使使用了 SET NOCOUNT ON,该函数也会被更新。因此,如果存储过程中执行的上一条 SQL 语句是实际的 UPDATE 语句,并且已经指定 @@ROWCOUNT 作为返回值,则可以对应用程序代码进行如下修改: // 向数据适配器的 UPDATE 命令添加另一个参数来接收返回值。 // 可以任意命名该参数。 myParm = da.UpdateCommand.Parameters.Add("@RowCount", SqlDbType.Int); myParm.Direction = ParameterDirection.ReturnValue; // 将 OnRowUpdated 方法修改为检查该参数的值 // 而不是 RecordsAffected 属性。 protected static void OnRowUpdated(object sender, SqlRowUpdatedEventArgs args) { if (args.Command.Parameters["@RowCount"].Value == 0) { args.Row.RowError = "遇到开放式并发冲突"; args.Status = UpdateStatus.SkipCurrentRow; } } 避免并发冲突: 1. 当where字句中使用了确保可以返回一行的条件,例如一个主关键字,这样可以确保 UPDATE 语句更新正确的行。 2. 如果你的where条件中使用的列有可能存在空值,例如where col1=@col1 and col2=@col2 ,如果col2列为null,那么where条件col2=@col2,无论您给@col2传递了什么值,如果col2的值为null,都将导致where不能匹配到行,这将导致异常,错误提示类似于:并发冲突:UpdateCommand 更新 0 个记录!当你在where条件中使用的列允许空值时,请使用如下写法: UPDATE Table1 Set Column1 = @NewColumn1Value WHERE (@OldColumn1Value IS NULL AND Column1 IS NULL) OR Column1 = @OldColumn1Value
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值