6.6 事务
事务((tansaction)是一种机制、一种操作序列,它包含了一组数据库操作命令,这组命令要么全部执行,要么全部不执行。可以简单地认为事务就是一组SQL语句,这组SQL语句是一个不可分割的工作逻辑单元,其结果应该作为一个整体永久性地修改数据库的内容,或者作为一个整体取消对数据库的修改。
在数据库系统上执行并发操作时,事务是作为最小的控制单元来使用的。这特别适用于多用户同时操作的数据通信系统。例如,订票、银行、保险公司以及证券交易系统等。
6.6.1 事务概述
事务的一个常用的例子就是将钱从一个银行账号中(A账号)转到另外一个银行账号中(B账号)。此时,它通常包含两步操作:
1)用一条UPDATE语句负责从A账号的总额中减去一定的钱数。
2)用另外一条UPDATE语句负责向B账号中增加相应的钱数。
值得注意的是,减少和增加这两个操作必须永久性地记录到数据库中,否则钱就会丢失。如果钱的转账有问题,则必须同时取消减少和增加这两个操作。这个简单的例子只使用了两个UPDATE语句,然而更实际的事务通常都可以包含多个INSERT、UPDATE和DELETE语句。
因此,事务有4个基本的特性,通常也被称为ACID特性(其中,ACID来自于每个特性的首字母):
1)原子性((Aomic):事务必须是原子工作单元,即一个事务中包含的所有SQL语句都是一个不可分割的工作单元。对于其数据修改,要么全都执行,要么全都不执行。通常,与某个事务关联的操作具有共同的目标,并且是相互依赖的。如果系统只执行这些操作的一个子集,则可能会破坏事务的总体目标。原子性消除了系统处理操作子集的可能性。
2)一致性((Cnsist):事务必须确保数据库的状态保持一致,这就是说事务开始时,数据库的状态是一致的;在事务结束时,数据库的状态也必须是一致的。在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。事务结束时,所有的内部数据结构(如B树索引或双向链表)都必须是正确的。某些维护一致性的责任由应用程序开发人员承担,他们必须确保应用程序已强制所有已知的完整性约束。例如,当开发用于转账的应用程序时,应避免在转账过程中任意移动小数点。
3)隔离性((Iolated):简单地讲,就是说多个事务可以独立运行,而不会彼此产生影响。无论在什么情况下,由并发事务所作的修改必须与任何其他并发事务所作的修改隔离。事务查看数据时,数据所处的状态要么是另一并发事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看中间状态的数据,这也称为可串行性。因为它能够重新装载起始数据,并且重播一系列事务,以使数据结束时的状态与原始事务执行的状态相同。当事务可序列化时将获得最高的隔离级别,在此级别上,从一组可并行执行的事务获得的结果与通过连续运行每个事务所获得的结果相同。由于高度隔离会限制可并行执行的事务数,所以一些应用程序降低隔离级别以换取更大的吞吐量。
4)持久性((Drable):一旦事务被提交之后,数据库的变化就会被永远保留下来,即使运行数据库软件的机器后来崩溃也是如此。
尽管事务提供了很强大的功能,但还是要谨慎地使用它。因为过多地使用事务会给系统带来很大的额外负担。另外,事务会锁定表中的某行。这样,不必要的事务会损害应用程序的性能。因此,在使用事务时,进行如下建议:
1)使事务尽量短、简单,提高执行效率。
2)尽量避免影响大批记录的更新。
3)避免使用具有多个独立批处理任务的事务。如果确实需要处理这样的事务,可以把各个批处理任务分解成单个事务进行处理。
4)尽量使用数据库事务来处理任务。
5)尽量避免在事务中使用SELECT语句返回数据,除非语句依赖于返回数据。如果使用SELECT语句,只选择需要的行,这样不会锁定过多的资源,从而尽可能地提高性能。
6.6.2 .NET事务的类型划分
如果按照事务是否跨越多个数据资源来分类,那么可以把事务划分为本地事务与分布式事务两种类型。其中:
1)本地事务是其范围为单个可识别事务的数据资源的事务(例如,Microsoft SQL Server数据库或MSMQ消息队列),即事务属于单阶段事务,并且由数据库直接处理。例如,当单个数据库系统拥有事务中涉及的所有数据时,就可以遵循ACID规则。在SQL Server的情况下,由内部事务管理器来实现事务的提交和回滚操作。
2)分布式事务可以跨越不同种类的可识别事务的数据资源,并且可以包括多种操作(例如,从SQL数据库检索数据、从Message Queue Server读取消息以及向其他数据库进行写入)。通过利用跨若干个数据资源来协调提交和中止操作以及恢复的软件,可以简化分布式事务的编程。Microsoft Distributed Transaction Coordinator(分布式事务协调器,DTC)就是这样一种技术,它采用一个二阶段的提交协议,该协议可确保事务结果在事务中涉及的所有数据资源之间保持一致。DTC只支持已实现了用于事务管理的兼容接口的应用程序。这些应用程序被称为资源管理器,目前存在许多这样的应用程序,包括MSMQ、Microsoft SQL Server、Oracle、Sybase等。
如果按事务处理方式划分,那么可以将事务划分为手动事务与自动事务两种类型。其中:
1)手动事务使你可以使用开始和结束事务的显式指令来显式控制事务边界。除此之外,它还允许你从活动事务中开始一个新事务的嵌套事务。但是,应用此控制会增加一种额外负担,需要向事务边界登记数据资源并对这些资源进行协调。
2)自动事务是通过有组件声明事务特性,把组件自动置于事务环境中。.NET Framework依靠MTS/COM+服务来支持自动事务。COM+使用DTC作为事务管理器和事务协调器在分布式环境中运行事务。这样可使.NET应用程序运行跨多个资源结合不同操作[例如,将定单插入SQL Server数据库、将消息写入Microsoft Message Queue(微软消息队列,MSMQ)、发送电子邮件以及从Oracle数据库检索数据]的事务。
通过提供基于声明性事务的编程模型,COM+使应用程序可以很容易地运行跨不同种类的资源的事务。这种做法的缺点是,由于存在DTC和COM互操作性开销,导致性能降低,而且不支持嵌套事务。
6.6.3 存储过程事务
这类事务完全在数据库中进行处理,也可称为数据库事务。它在存储过程中直接使用Begin Transaction、Rollback Transaction与Commit Transaction来实现事务。其中:
1)Begin Transaction:为一个连接标记出显式事务的起始点。
2)CommitTransaction:在没有出现错误时成功结束事务。由事务修改的所有数据都会永久成为数据库的一部分。事务所持有的资源将被释放。
3)RollbackTransaction:清除出现错误的事务。由事务修改的所有数据都将返回到事务启动时的状态。事务所持有的资源将被释放。
如在Microsoft SQL Server 2005或者更高的版本中,可以通过如下事务代码模型来满足上面银行转账的例子:
create procedure TMoney
(
@MAmount money,
@A int,
@B int
)
as
begin try
begin transaction
update Account set balance=balance+@MAmount
where accountid=@A
update Account set balance=balance-@MAmount
where accountid=@B
——事务提交
commit
end try
begin catch
if(@@trancount>0)
——事务回滚
rollback
——在这里可以使用RAISERROR来返回用户定义的错误信息并设系统标志,记录发生错误
end catch
如上面的代码所示,因为存储过程只需要往返一次数据库,并且所有的活动都在数据源进行,不需要任何网络通信。因此,它的性能非常高,所花费的代价也非常小,并且它还有一个优点是独立于应用程序。除了这些优点之外,它也有一些不足之处。首先,存储过程上下文仅在数据库中调用,这样就难以实现复杂的业务逻辑;其次,数据库事务代码与具体的数据库系统有关。所以说这类事务是一些小型事务处理程序首要考虑的方案。
最后需要说明的是,在SQL Server中,存储过程还可以执行分布式事务。默认情况下,所有事务从本地事务开始。但是当访问到其他服务器上的数据库时,事务自动升级为由Windows DTC服务掌控的分布式事务。
6.6.4 ADO.NET本地事务
这类事务是通过编程来处理的,本质上和存储过程事务一样,都有大致相同的命令,唯一不同的是这类事务使用的是封装了这些细节的ADO.NET对象。
在ADO.NET中,可以使用Connection对象和Transaction对象来控制事务。可以使用Connection.BeginTransaction启动本地事务。一旦开始一个事务,就可以使用Command对象的Transaction属性在该事务中登记命令。然后,可以根据事务组件的成功或失败情况,使用Transaction对象提交或回滚在数据源中所做的修改。
其中,Transaction类有两个关键的方法:
1)Commit():该方法与SQL事务中的Commit一样,表示成功完成事务。一旦调用这个方法,且该方法没有返回错误,则所有挂起更改都将写入底层数据库。具体实现依靠数据提供程序,但是通常都转换为在底层数据库执行Commit语句。
2)Rollback():该方法与SQL事务中的Rollback一样,表示未成功实现事务,同时删除挂起更改。数据库状态保持不变,从挂起状态回滚事务。
如下面的示例所示,该事务由try块中两个独立的命令组成。这两个命令对数据库ASPNET4的Employee表执行Insert语句,如果没有引发异常,则提交。如果引发异常,catch块中的代码将回滚事务。如果在事务完成之前事务中止或连接关闭,事务将自动回滚。
public partial class WebForm1:System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
string connectionString=
WebConfigurationManager.ConnectionStrings
["ConnectionString"].ConnectionString;
using(SqlConnection connection=
new SqlConnection(connectionString))
{
connection.Open();
//启动一个本地事务
SqlTransaction sqlTran=
connection.BeginTransaction();
SqlCommand command=connection.CreateCommand();
command.Transaction=sqlTran;
try
{
command.CommandText=
"insert into employee(employeeid, employeename,
department, address, email)values(6,'马伟2',
'软件研发部','陕西西安','madengwei@163.com')";
command.ExecuteNonQuery();
command.CommandText=
"insert into employee(employeeid, employeename,
department, address, email)values(7,'马伟2',
'软件研发部','陕西西安','madengwei@163.com')";
command.ExecuteNonQuery();
//事务提交
sqlTran.Commit();
Label1.Text="数据写入成功";
}
catch(Exceptionex)
{
Label1.Text=ex.Message+"<br>";
try
{
//事务回滚
sqlTran.Rollback();
}
catch(Exception exRollback)
{
Label1.Text=exRollback.Message;
}
}
}
}
}
根据上面的示例代码,可以按照下列步骤来执行事务:
1)调用Connection对象的BeginTransaction方法,以标记事务的开始。BeginTransaction方法返回对事务的引用。此引用分配给在事务中登记的Command对象。
2)将Transaction对象分配给要执行的Command的Transaction属性。如果在具有活动事务的连接上执行命令,并且尚未将Transaction对象分配给Command对象的Transaction属性,则会引发异常。
3)执行所需的命令。
4)调用Transaction对象的Commit方法完成事务,或调用Rollback方法结束事务。如果在Commit或Rollback方法执行之前连接关闭或断开,事务将回滚。
相比其他事务,这类事务的优点主要体现在:简单明了,事务可以跨越多个数据库访问,独立于数据库,不同数据库的专有代码被隐藏了,效率和存储过程事务差不多。当然,它也存在一些限制条件,如事务执行在数据库连接层上,所以需要在执行事务的过程中手动地维护一个连接。
6.6.5 隔离级别
隔离级别的概念与锁的概念密切相关,它决定了事务对其他事务影响的数据的敏感度。为事务指定一个隔离级别,该隔离级别定义一个事务必须与由其他事务进行的资源或数据更改相隔离的程度。隔离级别从允许的并发副作用(例如,脏读或幻读)的角度进行描述。
通常,事务隔离级别可以控制以下各项:
1)读取数据时是否占用锁以及所请求的锁类型。
2)占用读取锁的时间。
3)引用其他事务修改的行的读取操作与否,其中包括:在该行上的排他锁被释放之前阻塞其他事务;检索在启动语句或事务时存在的行的已提交版本;读取未提交的数据修改。
选择事务隔离级别不影响为保护数据修改而获取的锁。事务总是在其修改的任何数据上获取排他锁并在事务完成之前持有该锁,不管为该事务设置了什么样的隔离级别。对于读取操作,事务隔离级别主要定义保护级别,以防受到其他事务所做更改的影响。
1.SQL Server中的隔离级别
在SQL标准中,它将事务隔离级别划分为四个级别,由低到高分别为Read Uncommitted(未提交读取)、Read Committed(提交读取)、Repeatable Read(可重复读取)与Serializable(序列化)。其中,Read Uncommitted与Read Committed为语句级别的,而Repeatable Read与Serializable是针对事务级别的。
无论在Oracle还是SQL Server,都可以使用语句“Set Transaction Isolation Level”来设置事务隔离级别。如下所示:
Set Transaction Isolation Level Read Committed
SQL Server 2005及以上版本能够完全支持这些隔离级别:
1)Read Uncommitted,即未提交读取,它允许对数据执行未提交读或脏读,但不允许更新丢失。如果一个事务已经开始写数据,另外一个数据则不允许同时进行写操作,但允许其他事务读此行数据。图6-20所示是数据库ASPNET4中Employee表的原始数据。
图 6-20 原始的Employee表
为了演示Read Uncommitted隔离级别的运行效果,接下来,需要来新建两个连接:在第一个连接中执行以下语句:
select*from Employee
begin tran
update Employee set employeename='mawei'where employeeid=1
select*from Employee
waitfor delay'00:00:10'——等待10秒
rollback tran
select*from Employee
在第二个连接中执行以下语句:
Set Transaction Isolation Level Read Uncommitted
print'脏读'
select*from Employee
if@@rowcount>0
begin
waitfor delay'00:00:10'
print'不重复读'
select*from Employee
end
现在,同时执行这两个连接(先执行第一个连接,再执行第二个连接),其结果如图6-21与图6-22所示。
图 6-21 第一个连接的执行结果
在第二个连接中,因为将隔离级别设置成了“Read Uncommitted”,所以它读取了第一个连接中未提交的数据mawei,即产生了脏读。
2)Read Committed,即提交读取,它允许不可重复读取,但不允许脏读取。这可以通过“瞬间共享读锁”和“排他写锁”实现。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。它是SQL Server默认的级别。
图 6-22 第二个连接的执行结果
下面新建两个连接来演示Read Committed。其中,在第一个连接中执行以下语句:
Set Transaction Isolation Level Read Committed
select*from Employee
if@@rowcount>0
begin
waitfor delay'00:00:10'
select*from Employee
end
在第二个连接中执行以下语句:
update Employee set employeename='mawei'where employeeid=1
当同时执行这两个连接(先执行第一个连接,再执行第二个连接)时,第一个连接的结果如图6-23所示。
为了能够更加清楚地了解Read Uncommitted与Read Committed的区别,现在将Read Uncommitted示例中的第二个连接的隔离级别改为Read Committed。如下所示:
Set Transaction Isolation Level Read Committed
print'脏读'
select*from Employee
if@@rowcount>0
begin
waitfor delay'00:00:10'
print'不重复读'
select*from Employee
end
因为Read Committed不允许脏读取,它只能够读取提交的数据,所以它不会读取“mawei”,其运行结果如图6-24所示。
图 6-23 第一个连接的执行结果
图 6-24 提交读取
3)Repeatable Read,即可重复读取,它禁止不可重复读取和脏读取,但是有时可能出现幻影数据。这可以通过“共享读锁”和“排他写锁”实现。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。
下面新建两个连接来演示Repeatable Read。其中,在第一个连接中执行以下语句:
Set Transaction Isolation Level Repeatable Read
begin tran
print'初始'
select*from Employee
waitfor delay'00:00:10'——等待10秒
print'幻影数据'
select*from Employee
rollback tran
在第二个连接中执行以下语句:
insert into employee(employeeid, employeename,
department, address, email)values(6,'马伟2',
'软件研发部','陕西西安','madengwei@163.com')
当同时执行这两个连接(先执行第一个连接,再执行第二个连接)时,第一个连接便产生了幻影数据,其结果如图6-25所示。
图 6-25 第一个连接的执行结果
4)Serializable,即序列化,它提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行。如果仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。
如果将Repeatable Read中的示例的第一个连接的隔离级别改为“Serializable”,即“Set Transaction Isolation Level Serializable”,那么它将不会产生幻影数据,即第一个连接的执行结果如图6-26所示。
图 6-26 改为“Serializable”后,第一个连接的执行结果
较低的隔离级别可以增强许多用户同时访问数据的能力,但也增加了用户可能遇到的并发副作用(例如脏读或丢失更新)的数量。相反,较高的隔离级别减少了用户可能遇到的并发副作用的类型,但需要更多的系统资源,并增加了一个事务阻塞其他事务的可能性。所以,数据库隔离级别的选取就显得尤为重要,在选取数据库的隔离级别时,应该注意以下几个处理的原则:
1)必须排除“Read Uncommitted(未提交读取)”,因为在多个事务之间使用它将会是非常危险的。事务的回滚操作或失败将会影响到其他并发事务。第一个事务的回滚会完全将其他事务的操作清除,甚至使数据库处在一个不一致的状态。很可能一个已回滚为结束的事务对数据的修改最后却修改提交了,因为“未提交读取”允许其他事务读取数据,最后整个错误状态在其他事务之间传播开来。
2)绝大部分应用都无须使用“Serializable(序列化)”隔离(一般来说,读取幻影数据并不是一个问题),此隔离级别也难以测量。目前使用序列化隔离的应用中,一般都使用悲观锁,强行使所有事务都序列化执行。
3)在“Read Committed(提交读取)”和“Repeatable Read(可重复读取)”之间,如果无特殊需求,建议可以优先考虑把数据库系统的隔离级别设为Read Committed,它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、虚读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。
2.Oracle中的隔离级别
与SQL Server不同,Oracle并不完全支持这四种标准的事务隔离级别,它只提供了Read Committed、Serializable和Read Only三种事务隔离级别。其中,Read Only不是SQL标准的事务隔离级别,它只是Serializable的子集。无论Serializable,还是Read Only,它们都避免了非重复读和幻影数据。但两者区别在于,在Read Only中是只读,不允许在本事务中进行DML操作;而在Serializable中可以进行DML操作。
在Oracle中,没有了Read Uncommitted及Repeatable Read隔离级别。这样,在Oracle中就不允许一个会话读取其他事务未提交的数据修改结果,从而避免了由于事务回滚而发生的读取错误。虽然在Oracle中,Read Committed和Serializable级别的含义与SQL Server类似,但是实现方式却大不一样。
在Oracle中,存在所谓的回滚段或撤销段,Oracle在修改数据记录时,会把这些记录被修改之前的结果存入回滚段或撤销段中。就是因为这种机制,Oracle对于事务隔离级别的实现与SQL Server截然不同。在Oracle中,读取操作不会阻碍更新操作,更新操作也不会阻碍读取操作,这样在Oracle中的各种隔离级别下,读取操作都不会等待更新事务结束,更新操作也不会因为另一个事务中的读取操作而发生等待,这也是Oracle事务处理的一个优势所在。
与SQL Server一样,Oracle默认的设置也是Read Committed隔离级别。在这种隔离级别下,如果一个事务正在对某个表进行DML操作,而这时另外一个会话对这个表的记录进行读取操作,则Oracle会去读取回滚段或撤销段中存放的更新之前的记录,而不会像SQL Server一样等待更新事务的结束。
在Serializable隔离级别,事务中的读取操作只能读取这个事务开始之前已经提交的数据结果。如果在读取时,其他事务正在对记录进行修改,则Oracle就会在回滚段或撤销段中去寻找对应的、原来未经更改的记录(而且是在读取操作所在的事务开始之前存放于回滚段或撤销段的记录),这时读取操作也不会因为相应记录被更新而等待。因此,Serializable隔离级别提供了Read Only事务所提供的读一致性(事务级的读一致性),同时又允许DML操作。
3.ADO.NET中的隔离级别
在ADO.NET中,若要为事务设置隔离级别,可以使用Connection对象的BeginTransaction()方法来传入IsolationLevel枚举值,如表6-10所示。
在显式更改之前,IsolationLevel枚举值保持有效,但是也可以随时对它进行更改。新值在执行时使用,而不是在分析时使用。如果在事务期间更改,服务器的预期行为是对其余所有语句应用新的锁定级别。表6-11总结了不同隔离级别之间的锁的行为。
6.6.6 SQL Server保存点
我们知道,在事务执行回滚((Rllback)时,它将取消事务所提交的所有操作,并回到启动状态。但往往这并不是我们所需要的,我们更加希望事务执行回滚时只回滚正在执行事务的一部分(或者说某个点)。要实现这样的功能,可以利用保存点的特性来处理这种情况。
保存点是一种事务执行标记,即在事务的某一点做一个标记,以后事务就可以回滚到该点。但值得注意的是,保存点只在SQL Server中才有用,可以通过SqlTransaction的Save()方法来标记保存点。Save()方法不是标准的IDbTransaction接口,所以其他数据提供程序没有提供此方法。因此,Save()方法也仅仅在SqlTransaction中有用。保存点的使用方法如下面的代码所示:
SqlTransaction sqlTran=connection. BeginTransaction();
//为事务设置一个保存点
sqlTran. Save("UpdateEmployee");
//回滚到该保存点
sqlTran. Rollback("UpdateEmployee");
这样,只需要给Rollback()方法传相应的保存点名称作为参数,事务就可以回滚到该保存点。如果要想回滚整个事务,直接使用Rollback()方法,不用传入任何保存点作为参数。当事务回滚到某个保存点时,其后定义的所有保存点将丢失。
6.6.7 System.Transactions
System. Transactions基础结构通过支持在SQL Server、ADO.NET、MSMQ和Microsoft分布式事务协调器((MDTC)中启动的事务,使事务编程在整个平台上变得简单和高效。它提供基于Transaction类的显式编程模型,还提供使用TransactionScope类的隐式编程模型。在这种模型中,事务是由基础结构自动管理的。其中,TransactionScope可以使代码块成为事务性代码,并自动提升为分布式事务。
通过System.Transactions来处理事务,只需要简单的几行代码,不需要继承,不需要Attribute标记。用户根本不需要考虑是简单事务还是分布式事务。新模型会自动根据事务中涉及的对象资源判断使用何种事务管理器。简而言之,对于任何事务,用户只要使用同一种方法进行处理即可。
要使用System.Transactions,首先需要在项目中引用System.Transactions.dll,如图6-27所示。
图 6-27 引用System.Transactions.dll
然后在程序中添加“using System.Transactions;”命名空间引用。再把需要的事务性代码封装在一个using语句内,这个using语句会创建一个TransactionScope对象,最后,在事务结束时调用Complete方法。其中,Complete方法指示范围中的所有操作都已成功完成。当应用程序完成它要在一个事务中执行的所有工作以后,应当只调用Complete方法一次,以通知事务管理器可以接受提交事务。如下面的代码所示:
using(TransactionScope tran=new TransactionScope())
{
//事务操作代码
tran.Complete();
}
其实,这种用法是最简单,也是最常见的用法。创建了新的TransactionScope对象后,即开始创建事务范围。位于using块内的所有操作将成为一个事务的一部分,因为它们共享其所定义的事务执行上下文。而最后调用TransactionScope的Complete方法,将导致退出该块时请求提交该事务。此方法还提供了内置的错误处理,出现异常时会终止事务。
下面的示例演示了如何将两条数据库命令转换为一个事务,方法很简单,就是构建一个封装器把这两条命令封装起来:
using(TransactionScope tran=new TransactionScope())
{
//执行第一个数据库命令
using(SqlConnection con1=
new SqlConnection(connectionString1))
{
SqlCommand cmd=new SqlCommand(sqlUpdate, con1);
con1.Open();
cmd.ExecuteNonQuery();
}
//执行第二个数据库命令
using(SqlConnection con2=
new SqlConnection(connectionString2))
{
SqlCommand cmd=new SqlCommand(sqlDelete, con2);
con2.Open();
cmd.ExecuteNonQuery();
}
tran.Complete();
}
在上面的示例代码中,只要其中任意一个SqlCommand对象引发异常,程序流控制就会自动跳出TransactionScope的using语句块。随后,TransactionScope将自行释放并回滚该事务。由于这段代码使用了using语句,所以SqlConnection对象和TransactionScope对象都将被自动释放。由此可见,只需添加很少的几行代码,就可以构建出一个事务模型,这个模型可以对异常进行处理,执行结束后会自行清理。此外,它还可以对命令的提交或回滚进行管理。
在上面的示例代码中,con1和con2是两个不同的连接对象,分别连接到两个不同的数据库。因此,它将自动激活一个DTC管理的分布式事务(可以通过打开“管理工具”里面的组件服务,来查看当前的分布式事务列表)。
1.事务释放
其实,用好System.Transactions的关键就在于了解事务如何结束以及该何时结束。如果一个TransactionScope对象没有被正确释放,那么这个事务将保持打开状态,直到这个对象被垃圾收集器所收集,或者已超过超时时间为止。对打开的事务置之不理是有一定危险性的,其中一项危险就是处于活动状态的事务会锁定资源管理器的资源。下面这段代码或许能帮助你更好地理解这个问题:
TransactionScope tran=new TransactionScope();
SqlConnection con=new SqlConnection(cnString);
SqlCommand cmd=new SqlCommand(updateSql, con);
con.Open();
cmd.ExecuteNonQuery();
con.Close();
tran.Complete();
这段代码会创建TransactionScope对象的一个实例,当SqlConnection打开后,它将加入到该事务中。如果一切顺利,该命令将得到执行,连接将会关闭,事务将会完成,而且它也会被释放掉。但是,如果运行过程引发异常,那么程序流控制就会跳过关闭SqlConnection和释放TransactionScope的操作,导致该事务在比预期更长的时间内保持打开状态。因此,重中之重就是要确保正确释放TransactionScope,使事务要么快速提交,要么快速回滚。通常,面对这种情况时,可以通过两种简单的方法来处理这个问题:
1)使用try/catch/finally代码块。可以在try/catch/finally代码块之外声明这些对象,在try代码块中添加代码来创建对象并执行命令,并将对TransactionScope和SqlConnection的释放放到finally代码块中。这种方法可以确保事务及时关闭。
2)使用using语句。建议使用using语句,因为它能够隐性地创建一个try/catch代码块。使用using语句时,即便代码块中途引发异常,using语句也能够保证TransactionScope将会被释放。无论何时退出代码块,using语句都会确保已调用了TransactionScope的Dispose方法。这一点非常重要,因为就在释放TransactionScope之前,该事务已经完成了。事务完成时,TransactionScope就会判断是否已经调用了Complete方法。如果已经调用,那么该事务就会被提交;否则,该事务就会回滚。因此,可以将上面的代码修改成如下形式:
using(TransactionScope tran=new TransactionScope())
{
using(SqlConnection con=new SqlConnection(cnString)
{
SqlCommand cmd=new SqlCommand(updateSql, con);
con.Open();
cmd.ExecuteNonQuery();
}
tran.Complete();
}
在这里,对TransactionScope对象和SqlConnection对象都使用了using语句。这样做是为了确保一旦引发异常,这两个对象都可以得到快速、正确的释放。如果代码块没有引发异常,那么这两个对象将在using语句代码块结束时(最后一个大括号的位置)被释放。
2.事务设置
若要更改TransactionScope类的默认设置,可以创建一个TransactionOptions对象,然后通过它在TransactionScope对象上设置隔离级别和事务的超时时间。TransactionOptions类有一个IsolationLevel属性,通过这个属性可以更改事务的隔离级别,该属性默认情况下为Serializable。此外,TransactionOptions类还有一个TimeOut属性,这个属性可以用来更改超时时间,该属性默认设置为1分钟。
除此之外,还可以通过设置TransactionScopeOption枚举值来传递给TransactionScope类的各个构造函数,以定义范围的事务性行为。其中,TransactionScopeOption枚举值有如下三个:
1)Required:该范围需要一个事务。如果已经存在环境事务,则使用该环境事务。否则,在进入范围之前创建新的事务。这是默认值。
2)RequiresNew:总是为该范围创建新事务。
3)Suppress:环境事务上下文在创建范围时被取消。范围中的所有操作都在无环境事务上下文的情况下完成。如果想要保留代码部分执行的操作,并且在操作失败的情况下不希望中止环境事务,则Suppress很有帮助。例如,在想要执行日志记录或审核操作时,不管环境事务是提交还是中止,上述值都很有用。该值允许你在事务范围内具有非事务性的代码部分,如下面的示例所示:
using(TransactionScope tran=new TransactionScope())
{
//开始一个非事务范围
using(TransactionScope tran1=new TransactionScope
((TansactionScopeOption.Suppress))
{
//这里不受事务控制代码
}
//从这里开始又回归事务处理
}
3.事务嵌套
前文已经阐述了TransactionScopeOptions枚举值,当遇到嵌套方法和事务时,这个TransactionScopeOptions就能起到作用了。举例来说,假设Method1创建一个TransactionScope,针对一个数据库执行一条命令,然后调用Method2。Method2创建一个自身的TransactionScope,并针对一个数据库执行另一条命令。可以通过多种方法来处理这个问题。你可能希望Method2的事务加入到Method1的事务中,也可能想让Method2创建一个属于自己的单独的事务。在这种情况下,TransactionScopeOptions的价值就得到了充分体现。如下面的代码所示:
private void Method1()
{
using(TransactionScope tran=new TransactionScope
((TansactionScopeOption.Required))
{
using(SqlConnection con=new SqlConnection())
{
SqlCommand cmd=new SqlCommand(updateSql1,con);
con.Open();
cmd.ExecuteNonQuery();
}
//子事务方法
Method2();
tran.Complete();
}
}
private void Method2()
{
using(TransactionScope tran=
new TransactionScope(TransactionScopeOption.RequiresNew))
{
using(SqlConnection con=new SqlConnection())
{
SqlCommand cmd=new SqlCommand(updateSql2,con);
con.Open();
cmd.ExecuteNonQuery();
}
tran.Complete();
}
}
在上面的代码中,内层事务Method2将创建出第二个TransactionScope,而不是加入外层事务Method1。Method2的TransactionScope是使用RequiresNew设置创建的,这也就是告诉这个事务要创建自己的范围,而不是加入一个已有的范围。如果希望这个事务加入到已有事务中,则可以保留默认设置不变,或者将该选项设置为Required。
事务加入到一个TransactionScope(因为它们使用了Required设置)中后,只有它们全部投票,事务才能成功完成((Cmplete),也才能提交事务。在同一个TransactionScope中,如果任何一个事务没有调用tran.Complete,也就是说没有投票完成,那么当外层的TransactionScope被释放后,它将会回滚。
4.在分布式事务中登记
在ADO.NET中,还可以使用Connection对象的EnlistTransaction方法在分布式事务中登记。在一个事务中显式登记了某个连接后,如果第一个事务尚未完成,则无法取消登记或在另一个事务中登记该连接。由于EnlistTransaction在Transaction实例中登记连接,因此,该方法利用System.Transactions命名空间中的可用功能来管理分布式事务。示例如下面的代码所示:
private void Method1()
{
CommittableTransaction ctran=new CommittableTransaction();
using(SqlConnection con=new SqlConnection(conString))
{
con.EnlistTransaction(ctran);
}
ctran.Commit();
}
其中,CommittableTransaction类为应用程序使用事务提供了一种显式方法,而不是隐式地使用TransactionScope类。对于要跨多个函数调用或多个线程调用使用同一事务的应用程序,前一种类十分有用。与TransactionScope类不同,应用程序编写器需要明确调用Commit和Rollback方法以提交或中止事务。但是,只有事务的创建者才能提交事务。
6.6.8 COM+事务
前面已经阐述过,.NET Framework就是依靠MTS/COM+服务来支持自动事务处理。COM+使用DTC作为事务管理器和事务协调器在分布式环境中运行事务。这样可使.NET应用程序运行跨多个资源结合不同操作(例如将定单插入SQL Server数据库、将消息写入Microsoft消息队列((MMQ)队列,以及从Oracle数据库检索数据)的事务。
要实现COM+事务处理的类,则必须继承System.EnterpriseServices.ServicedComponent类,它是COM+服务的所有类的基类。事务处理类里面的每个方法都会运行在一个事务中。其实,大家知道的Web Service就是继承自ServicedComponent类。因此,Web Service也支持COM+事务。除了需要继承ServicedComponent类之外,还需要在类定义之前添加一个Transaction属性,例如“[Transaction(TransactionOption.Required)]”。创建示例如下面的代码所示:
using System;
using System.Collections.Generic;
using System.Text;
using System.EnterpriseServices;
namespace ComTest
{
[Transaction(TransactionOption.Required)]
public class MyCom:ServicedComponent
{
}
}
其中,TransactionOption枚举值如表6-12所示。
一般情况下,只需要将TransactionOption设置成Required或Supported就可以了。当组件用于记录或查账时,RequiresNew就很有用,因为组件应该与活动中其他事务处理的提交或回滚隔离开来。
COM+事务支持两种处理方式,即手动处理方式和自动处理方式。自动处理就是在所需要自动处理的方法前加上[AutoComplete],根据方法的正常或抛出异常决定提交或回滚。手动处理就是调用ContextUtil类中的EnableCommit、SetComplete和SetAbort方法。下面通过一个实际例子来详细阐述如何创建和使用COM+事务处理的类。
1.创建类库ComTest
要创建COM+事务处理的类,就需要给程序添加一个强名称。强名称的创建方法如下:
1)使用创建密钥的工具sn.exe来创建一对密钥。可以通过命令提示来运行它,该工具可执行各种任务以生成并提取密钥,如图6-28所示。
图 6-28 使用sn.exe工具创建密钥
通过在图6-28中执行“sn-k c:\mykey.snk”命令,就在C盘下面生成了一个密钥文件mykey.snk。mykey.snk代表将保存密钥的文件的名称。它的名称可以是任意的,不过习惯上带有.snk后缀名。
2)将mykey.snk文件复制到项目的根文件夹下,打开项目属性进行设置(即签名),如图6-29所示。签名通常是在编译时进行的,签名时,可利用C#属性通知编译器应该使用正确的密钥文件对DLL进行签名。
图 6-29 设置mykey.snk
3)为类库ComTest添加好强名称之后,需要创建两个COM+事务处理的类。其中,MyCom类采用了手动事务处理方式,如代码清单6-2所示。
代码清单6-2 MyCom.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.EnterpriseServices;
using System.Data.SqlClient;
using System.Configuration;
namespace ComTest
{
[Transaction(TransactionOption.Required)]
public class MyCom:ServicedComponent
{
private string connectionString=
ConfigurationManager.ConnectionStrings
["ConnectionString"].ConnectionString;
private void Insert()
{
using(SqlConnection myConnection=new
SqlConnection(connectionString))
{
string sql="insert into employee(employeeid,
employeename, department, address, email)values
(10,'马伟2','软件研发部','陕西西安','madengwei@163.com')";
SqlCommand myCommand=new
SqlCommand(sql, myConnection);
myConnection.Open();
int rows=myCommand.ExecuteNonQuery();
}
}
private void Delete()
{
using(SqlConnection myConnection=
new SqlConnection(connectionString))
{
string sql="delete from employee where
employeeid=10";
SqlCommand myCommand=new
SqlCommand(sql, myConnection);
myConnection.Open();
int rows=myCommand.ExecuteNonQuery();
}
}
public string WorkTran()
{
try
{
ContextUtil.EnableCommit();
Insert();
Delete();
ContextUtil.SetComplete();
return"成功!";
}
catch(Exception ex)
{
ContextUtil.SetAbort();
return ex.Message;
}
}
}
}
AutoMyCom类采用了自动事务处理方式,即在WorkTran()方法前添加[AutoComplete(true)]属性,这样如果方法执行时没有异常就默认提交,如果有异常则这个方法就会回滚。如代码清单6-3所示。
代码清单6-3 AutoMyCom.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.EnterpriseServices;
using System.Data.SqlClient;
using System.Configuration;
namespace ComTest
{
[Transaction(TransactionOption.Required)]
public class AutoMyCom:ServicedComponent
{
private string connectionString=
ConfigurationManager.ConnectionStrings
["ConnectionString"].ConnectionString;
private void Insert()
{
using(SqlConnection myConnection=new
SqlConnection(connectionString))
{
string sql="insert into employee(employeeid,
employeename, department, address, email)values
(10,'马伟2','软件研发部','陕西西安','madengwei@163.com')";
SqlCommand myCommand=new SqlCommand
((sl, myConnection);
myConnection.Open();
int rows=myCommand.ExecuteNonQuery();
}
}
private void Delete()
{
using(SqlConnection myConnection=new
SqlConnection(connectionString))
{
string sql="delete from employee
where employeeid=10";
SqlCommand myCommand=new SqlCommand
((sl, myConnection);
myConnection.Open();
int rows=myCommand.ExecuteNonQuery();
}
}
//自动事务
[AutoComplete(true)]
public string WorkTran()
{
try
{
Insert();
Delete();
return"成功!";
}
catch(Exception ex)
{
return ex.Message;
}
}
}
}
创建这两个事务处理类之后,还需要把AssemblyInfo.cs文件中的ComVisible设为true,即[assembly:ComVisible(true)]。
2.创建应用示例
方法很简单,只需要在应用程序项目里引用一下ComTest.dll文件,就可以直接在程序里来调用这两个事务处理类了,如下面的示例代码所示:
protected void myCom_Click(object sender, EventArgs e)
{
ComTest.MyCom tran=new ComTest.MyCom();
Label1.Text=tran.WorkTran();
}
protected void autoMyCom_Click(object sender, EventArgs e)
{
ComTest.AutoMyCom tran=new ComTest.AutoMyCom();
Label1.Text=tran.WorkTran();
}
在使用COM+事务时,必须注意以下几点:
1)确保使用COM+服务的所有项目都有一个强名称。
2)确保使用COM+服务的所有类都必须继承System.EnterpriseServices.ServicedComponent类。
3)进行调试时,事务在提交或终止前可能会超时。要避免出现超时,可以在事务属性中使用一个超时属性Timeout,设置示例如下所示:
[Transaction(TransactionOption. Required, Timeout=1200)]
我们知道,COM+事务属于企业级的,它有许多优点,如执行分布式事务,多个对象可以轻松地运行在同一个事务处理中,事务处理还可以自动登记;获得COM+服务,诸如对象构建和对象池等。尽管如此,还是需要小心使用它,或者说是尽量少用它。因为它也有一定的缺陷,如由于存在DTC和COM互操作性开销,导致性能降低;使用Enterprise Services的事务总是线程安全的,也就是说无法让多个线程参与到同一个事务中等。