(十)事务和并发
1、并发
1.1 概述
当多个用户试图同时修改数据时,需要建立控制机制来防止一个用户的修改对同时操作的其他用户所作的修改产生不利的影响。处理这种情况的系统称为“并发控制”。数据库访问的并发控制常见的有3种方式,在ADO.NET中可以使用前面的两种。
并发控制 | 说明 |
“后来者赢” | 只有当实际更新数据时,该行才对其他用户不可用。但是,不会将更新与初始记录进行比较;而只是写出记录,这可能就改写了自上次刷新记录后其他用户所进行的更改。这意味着如果两个用户编辑同一行,后面的编辑生效,而前面的修改会丢失。这是ADO.NET中的默认行为。也称“最后的更新生效” |
开放式并发控制 | 只有当实际更新数据时,该行才对其他用户不可用。更新将在数据库中检查该行并确定是否进行了任何更改。如果试图更新已更改的记录,则将导致并发冲突。。也称“先来者赢”。 |
悲观并发控制 | 在从获取记录直到记录在数据库中更新的这段时间内,该行对用户不可用。也就是说,行在检索时就被锁定,直到更新完毕才解除锁定,这可能影响性能,但可确保数据得到保护。这在ADO.NET中是不可能实现的。要实现它可以使用一些高级技术。也称“保守式并发控制” |
在多用户环境中,有两种用于更新数据库中数据的模型:开放式并发和保守式并发。 设计 DataSet 对象的目的是为了促进将开放式并发用于长时间运行的活动,例如对数据进行远程处理以及与数据进行交互时。
保守式并发涉及到锁定数据源中的行,以防止其他用户因修改数据而影响当前用户。 在保守式模型中,当用户执行会应用锁的操作时,其他用户将无法执行可能与锁发生冲突的操作,直到锁所有者释放锁为止。 此模型主要用于以下环境:对数据存在激烈争用,使得用锁保护数据的成本少于在发生并发冲突时回滚事务的成本。
因此,在保守式并发模型中,更新行的用户建立锁。 在该用户完成更新并释放锁之前,其他任何用户都无法更改锁定行。 因此,如果锁定时间将会比较短(例如在以编程方式处理记录时),最好实现保守式并发。 如果用户与数据进行交互,会使记录锁定相对长的时间,保守式并发并不是可伸缩的选项。
|
如果您需要在同一个操作中更新多个行,则创建事务要比使用保守式锁定更具伸缩性。 |
对比之下,使用开放式并发的用户在读取行时不会锁定该行。 当用户要更新某行时,应用程序必须确定自读取该行以来,其他用户是否更改了该行。 开放式并发通常用于对数据争用较少的环境。 由于不需要锁定任何记录,开放式并发将会提高性能,因为锁定记录需要更多的服务器资源。 另外,为了维护记录锁,需要与数据库服务器保持持久连接。 由于在开放式并发模型中并不会这样,所以与服务器的连接可以在较少的时间内为更多的客户端提供服务。
在开放式并发模型中,如果当某用户接收到来自数据库的值后,另一用户在该用户试图修改该值之前即将其修改,则认为发生了冲突。
(摘自:http://msdn.microsoft.com/zh-cn/library/aa0416cz.aspx“开放式并发(ADO.NET)”)
1.2 “后来者赢”
工作原理是,只用主键来标识数据库表中的行。这意味着如果在获取行后,它被修改或删除,你将不知道,因此将发生下列情况之一:
Ø 行被更新,但在你获取该行后的修改全部丢失;
Ø 不能更新行,因此要更新的行已删除。
如果使用“后来者赢”并发控件,应考虑包含实现以下功能的代码:
Ø 检查ExecuteNonQuery()方法调用的返回值,看数据修改是否成功;
Ø 发生并发冲突时采取适当的措施,如引发/处理异常,并通知用户出现了问题;
Ø 修改数据后刷新它,确保反映的是当前数据。
虽然如此,使用这种并发控件是有原因的。很多应用程序不必考虑并发问题,尤其是可以确定只有一个应用程序或用户使用数据库时。
1.3 ADO.NET中的开放式并发管理
开放式并发控制要求额外的实现,但通常在实现过程中可以获得帮助,因此不会太难。要在应用程序中使用开放式并发控制,有两种方式:
Ø 比较所有列值,确保更新的是行的当前版本。
Ø 使用版本号,或时间戳记录行的版本,并确保在更新行之前行的当前版本号或时间戳与记录的一致。
1.4 客户端应用程序中的并发冲突
发生冲突时,通常让用户进行选择操作。这时可以用一棵决策树来决定下一步如何做。下面是一种解决方案的决策树:
1.5 解决并发冲突
根据选定的决策树,通过直接编程实现,而不是使用数据库的技巧。
2、事务
2.1 概述
事务是一组相互关联的操作。要么执行全部操作,要么不执行任何一项操作。只要事务中有一项操作失败,就必须回滚此前的所有操作,以确保一致性。事务不仅仅用于数据库中。
数据库中的事务是可以自动提升(promotable)的事务,这意味着需要时将创建公布式事务。实际上,事务开始可以使用一种资源,然后再添加其他资源,这时事务将自动提升为分布式事务。
2.2 SQL事务
在存储过程中使用SQL代码创建和管理的事务。将所有事务代码存储在数据库中的存储过程中的效率比较高,性能比使用C#代码更高。使用SQL Server时,实际上一直在执行事务,因为SQL命令被解释为事务。
自动提交事务适用于单个命令,而不适用于一系列命令,如组成存储过程的命令。例如:
DELETE FROM Enchanment WHERE ID=1000
DELETE FROM Enchanment WHERE ID=1001
这里每个命令各使用一个事务,即使它们是一个命令块。这意味着第一个命令失败,第二个也能够执行。这与下面的两条命令有区别:
DELET FROM Enchanment WHERE ID=1000
DELETE FROM Enchanment WHERE ID=1001
第一条命令有语法错误,DELET不是SQL Server关键字。注意:语义错误(由于数据库限制、使用不存在的表名等,导致命令失败)与语法错误之间是有区别的。有语法错误时,SQL Server无法编译命令,因此这两条命令都不会被执行。
要将多个SQL命令作为事务执行,必须用关键字BEGIN TRANSACTION(或BEGIN TRAN)来定义事务的开始,然后用提交或回滚来定义事务的结束。SQL显式事务基本结构如下:
BEGIN TRANSACTION [transaction name]
…
COMMIT TRANSACTION [transaction name]
也可以将事务嵌在其他事务中,但这不会提供更多的功能。只是可以在使用显式事务的批命令中调用使用显式事务的存储过程。内嵌事务结束时并不提交修改,即使遇到COMMIT TRANSACTION 语句。仅当最外层的事务结束时,内嵌事务的修改才会提交。
在SQL事务中,仅当错误比较严重时才会导致事务回滚。很多错误(如添加包含重复主键的记录,如下例所示)并不会导致整个事务回滚。只有产生错误语句不能提交时, 才回滚整个事务,这与前面讨论的自动提交事务是一样的。
要修改这种行为有两种方式。
一是,使用SQL异常处理代码来检测错误,并在必要时使用ROLLBACK TRANSACTION 来回滚整个事务。然而,如果这样做,后继对COMMIT TRANSACTION的调用将导致错误,因为事务已经终止。故应在调用COMMIT TRANSACTION前检查变量@@TRANSACTION的值,具体如下:
BEGIN TRANSACTION
…
(code with might call ROLLBACK TRANSACTION)
…
IF @@TRANSACTION > 0
COMMIT TRANSACTION
二是,将数据库的XACT_ABORT选项设置为ON。这样,只要发生运行阶段错误(包括“不太严重”的错误),事务都将终止,导致所做的修改被回滚。在SQL事务处理过程中,锁定了被修改的行,因此在更新行时,其他代码不能干扰这些行的数据。
2.3 .NET事务
在C#代码中创建和使用的事务。可以在同一个事务中包含数据库访问以及访问其他资源的代码,且实现的代码相当简单,容易调试。通过使用System.Transactions命名空间中的类,可以实现所有的事务需求,包括ADO.NET。
在.NET中处理事务有两种方式。一是,获取SqlTransactions对象,并通过它执行操作;二是,使用TransactionScope和Transaction对象自动将数据库访问代码包含在事务中。
1) 使用SqlTransaction
SqlTransaction对象表示可以包含数据库访问代码的事务。创建和使用它的方法如下:
SqlConnection conn = new SqlConnection ( ConnectionString);
conn.open();
SqlTransaction tran = conn.BeginTransaction();
Try
{
//数据库操作代码。
…
//提交事务。
tran.Commit ();
}
Catch ( Exception ex )
{
//回滚事务
tran.Rollback();
//处理异常。
…
}
conn.Close();
这样获得的的SqlTransaction对象只能用于一个连接,因此不适用于分布式事务。
Rollback()方法也可能失败,如失去到数据库的连接,因此应将该方法的调用封装在另一个try…catch块中。
要作为事务一部分执行的命令也必须显式地放在事务中,可通过将SqlCommand.Transaction属性设置为SqlTransaction来实现。如果不这样做,命令的执行将产生异常,即使SqlConnection对象已经包含对事务的引用。
2) 使用TransactionScope和Transaction
这两个对象提供了一种在任何类型的.NET应用程序中处理事务代码的通用方式。其结构可以如下:
using ( TransactionScope tran = new TransactionScope() )
{
//事务处理代码,包括任何数据库访问代码。
…
}
也可以使用try…catch…finally块来确保该对象被释放。