事务是一组组合成逻辑工作单元的数据库操作,在系统执行过程中可能会出错,但事务将控制和维护每个数据库的一致性和完整性。事务处理的主要特征是,任务要么全部完成,要么都不完成。在写入一些记录时,要么写入所有记录,要么什么都不写入。如果在写入一个记录时出现了一个失败,那么在事务处理中已写入的其他数据就会回滚。事务可能由很多单个任务构成。
简单事务的一个常见例子:把钱从A账户转到B账户,这涉及两项任务,即从A账户把钱取出来;把钱存入B账户。两项任务要么同时成功,要么一起失败,给予回滚,以便保持账户的状态和原来相同。否则,在执行某一个操作的时候可能会因为停电、网络中断等原因而出现故障,所以有可能更新了一个表中的行,但没有更新相关表中的行。如果数据库支持事务,则可以将数据库操作组成一个事务,以防止因这些事件而使数据库出现不一致。
事务的ACID属性如下。
l 原子性(Atomicity):事务的所有操作是原子工作单元;对于其数据修改,要么全都执行,要么全都不执行。原子性消除了系统处理操作子集的可能性。
l 一致性(Consistency):数据从一种正确状态转换到另一种正确状态。事务在完成时,必须使所有的数据都保持一致。在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。当事务结束时,所有的内部数据结构都必须是正确的。在存款取款的例子中,逻辑规则是,钱是不能凭空产生或销毁的,对于每个(收支)条目必须有一个相应的抵衡条目产生,以保证账户是平的。
l 隔离性(Isolation):由并发事务所作的修改必须与任何其他并发事务所作的修改隔离。查看数据时数据所处的状态,要么是事务修改它之前的状态,要么是事务修改它之后的状态。简单的理解就是,防止多个并发更新彼此干扰。事务在操作数据时与其他事务操作隔离。隔离性一般是通过加锁的机制来实现的。
l 持久性(Durability):事务完成之后,它对于系统的影响是永久性的。已提交的更改即使在发生故障时也依然存在。
对于事务的开发,.NET平台也为我们提供了几种非常简单方便的事务机制。无论是在功能上还是性能上都提供了优秀的企业级事务支持。
.NET开发者可以使用以下5种事务机制:
l SQL和存储过程级别的事务。
l ADO.NET级别的事务。
l ASP.NET页面级别的事务。
l 企业级服务COM+事务。
l System.Transactions 事务处理。
这5种事务机制有着各自的优势和劣势,分别表现在性能、代码数量和部署设置等方面。开发人员可以根据项目的实际情况选择相应的事务机制。
5.4.1 SQL和存储过程级别的事务
数据库事务是其他事务模型的基础,当一个事务创建时不同数据库系统都有自己的规则。SQL Server默认在自动提交的模式下工作,每个语句执行完后都会立即提交;与此对照的是Oracle需要你包含一个提交语句。但是当一个语句通过OLE DB执行时,它执行完后一个提交动作会被附加上去。例如:
DECLARE @TranName VARCHAR(20);
SELECT @TranName = 'MyTransaction';
BEGIN TRANSACTION @TranName;
GO
USE AdventureWorks;
GO
DELETE FROM AdventureWorks.HumanResources.JobCandidate
WHERE JobCandidateID = 13;
GO
COMMIT TRANSACTION MyTransaction;
GO
或者:
CREATE PROCEDURE Tran1
as
begin tran
set xact_abort on
Insert Into P_Category(CategoryId,Name)values('1','test1')
Insert Into P_Category(CategoryId,Name)values('2','test2')
commit tran
GO
set xact_abort on表示遇到错误立即回滚。
当然你也可以这么写:
CREATE PROCEDURE tran1
as
begin tran
Insert Into P_Category(CategoryId,Name)values('1','test1')
if(@@error<>0)
rollback tran
else
begin
Insert Into P_Category(CategoryId,Name)values('2','test2')
if(@@error<>0)
rollback tran
else
commit tran
end
GO
数据库事务有它的优势和限制。
优势:
l 所有的事务逻辑包含在一个单独的调用中。
l 拥有运行一个事务的最佳性能。
l 独立于应用程序。
限制:
l 事务上下文仅存在于数据库调用中。
l 数据库代码与数据库系统有关。
5.4.2 ADO.NET级别的事务
现在我们对事务的概念和原理都有所了解了,并且作为已经有一些基础的C#开发者,我们已经熟知编写数据库交互程序的一些要点,即:
(1)使用SqlConnection类的对象的Open()方法建立与数据库服务器的连接。
(2)然后将该连接赋给SqlCommand对象的Connection属性。
(3)将欲执行的SQL语句赋给SqlCommand的CommandText属性。
(4)通过SqlCommand对象进行数据库操作。
创建一个ADO.NET事务是很简单的,需要定义一个SqlTransaction类型的对象。SqlConnection 和OleDbConnection对象都有一个 BeginTransaction 方法,它可以返回 SqlTransaction 或者OleDbTransaction 对象。然后赋给SqlCommand对象的Transcation属性,即实现了二者的关联。为了使事务处理可以成功完成,必须调用SqlTransaction对象的Commit()方法。如果有错误,则必须调用Rollback()方法撤销所有的操作。
基于以上认识,下面我们就开始动手写一个基于ADO.NET的事务处理程序。
(示例位置:光盘\code\ch05\04\Web\WebForm2)
string conString = "data source=127.0.0.1;database=codematic;user id=sa;
password=";
SqlConnection myConnection = new SqlConnection(conString);
myConnection.Open();
//启动一个事务
SqlTransaction myTrans = myConnection.BeginTransaction();
//为事务创建一个命令
SqlCommand myCommand = new SqlCommand();
myCommand.Connection = myConnection;
myCommand.Transaction = myTrans;
try
{
myCommand.CommandText = "update P_Product set Name='电脑2' where Id=52";
myCommand.ExecuteNonQuery();
myCommand.CommandText = "update P_Product set Name='电脑3' where Id=53";
myCommand.ExecuteNonQuery();
myTrans.Commit();//提交
Response.Write("两条数据更新成功");
}
catch (Exception ex)
{
myTrans.Rollback();//遇到错误,回滚
Response.Write(ex.ToString());
}
finally
{
myConnection.Close();
}
ADO.NET事务的优势和限制如下。
优势:
l 简单。
l 和数据库事务差不多快。
l 事务可以跨越多个数据库访问。
l 独立于数据库,不同数据库的专有代码被隐藏了。
限制:事务执行在数据库连接层上,所以需要在执行事务的过程中手动地维护一个连接。
注 意 |
所有命令都必须关联在同一个连接实例上,ADO.NET事务处理不支持跨多个连接的事务处理。 |
5.4.3 ASP.NET页面级别的事务
ASP.NET事务可以说是在.NET平台上事务实现方式最简单的一种,你仅仅需要一行代码即可。在aspx的页面声明中加一个额外的属性,即事务属性Transaction="Required",它有如下的值:Disabled(默认)、NotSupported、Supported、Required和RequiresNew,这些设置和COM+及企业级服务中的设置一样,典型的一个例子是如果你想在页面上下文中运行事务,那么要将其设置为Required。如果页面中包含有用户控件,那么这些控件也会包含到事务中,事务会存在于页面的每个地方。
(示例位置:光盘\code\ch05\04\ Web\WebForm3.aspx)
页面声明Transaction="Required":
<%@ Page Transaction="Required" Language="C#" AutoEventWireup="true"
CodeBehind="WebForm3.aspx.cs" Inherits="WebApplication4.WebForm3" %>
页面引用:using System.EnterpriseServices;。
然后,数据操作代码:
protected void Button1_Click(object sender, EventArgs e)
{
try
{
Work1();
Work2();
ContextUtil.SetComplete(); //提交事务
}
catch (System.Exception except)
{
ContextUtil.SetAbort(); //撤销事务
Response.Write(except.Message);
}
}
private void Work1()
{
string conString = "data source=127.0.0.1;database=codematic;user id=sa;
password=";
SqlConnection myConnection = new SqlConnection(conString);
string strSql = "Insert Into P_Category(CategoryId,Name)values('1',
'test1')";
SqlCommand myCommand = new SqlCommand(strSql, myConnection);
myConnection.Open();
int rows = myCommand.ExecuteNonQuery();
myConnection.Close();
}
private void Work2()
{
string conString = "data source=127.0.0.1;database=codematic;user id=sa;
password=";
SqlConnection myConnection = new SqlConnection(conString);
string strSql = "Insert Into P_Category(CategoryId,Name)values('2',
'test2')";
SqlCommand myCommand = new SqlCommand(strSql, myConnection);
myConnection.Open();
int rows = myCommand.ExecuteNonQuery();
myConnection.Close();
}
ContextUtil是用于获取 COM+ 上下文信息的首选类。由于此类的成员全部为static,因此在使用其成员之前不需要对此类进行实例化。
ASP.NET页面事务的优势和限制如下。
l 优势:实现简单,不需要额外的编码。
l 限制:页面的所有代码都是同一个事务,这样的事务可能会很大,而也许我们需要的是分开的、小的事务实现在Web层。
5.4.4 企业级服务COM+事务
.NET Framework 依靠 MTS/COM+ 服务来支持自动事务处理。COM+ 使用 Microsoft Distributed Transaction Coordinator(DTC)作为事务管理器和事务协调器在分布式环境中运行事务。这样可使 .NET 应用程序运行跨多个资源结合不同操作(例如将定单插入SQL Server 数据库、将消息写入 Microsoft 消息队列(MSMQ)队列,以及从 Oracle 数据库检索数据)的事务。
要实现COM+事务处理的类则必须继承System.EnterpriseServices.ServicedComponent,这些类需要是公共的,并且需要提供一个公共的默认的构造器。其实Web Service就是继承ServicedComponent,所以Web Service也支持COM+事务。要在类定义之前加属性[Transaction(TransactionOption.Required)]。类里面的每个方法都会运行在一个事务中。
定义一个COM+事务处理的类:
首先引用:using System.EnterpriseServices;。
然后,继承ServicedComponent。
[Transaction(TransactionOption.Required)]
public class OrderData : ServicedComponent
{
}
TransactionOption枚举类型支持5个值:Disabled、NotSupported、Required、RequiresNew和Supported,如表5-3所示。
表5-3 TransactionOption枚举类型支持5个值
值 |
说 明 |
Disabled |
忽略当前上下文中的任何事务 |
NotSupported |
使用非受控事务在上下文中创建组件 |
Required |
如果事务存在则共享事务,并且如有必要则创建新事务 |
RequiresNew |
使用新事务创建组件,而与当前上下文的状态无关 |
Supported |
如果事务存在,则共享该事务 |
一般来说COM+中的组件需要Required 或Supported。当组件用于记录或查账时RequiresNew 很有用,因为组件应该与活动中其他事务处理的提交或回滚隔离开来。
派生类可以重载基类的任意属性。如OrderData选用Required,派生类仍然可以重载并指定RequiresNew或其他值。
COM+事务有手动处理和自动处理两种方式,自动处理就是在所需要自动处理的方法前加上[AutoComplete],根据方法的正常或抛出异常决定提交或回滚。手动处理就是调用ContextUtil类中的EnableCommit、SetComplete和SetAbort方法。
实现步骤如下。
1.给程序添加强名
1)创建一对密钥
用来创建密钥的工具是称为sn.exe的共享工具。通常通过命令提示运行它,该工具可执行各种任务以生成并提取密钥。我们需要用以下方式来运行sn.exe。
sn –k c:\key.snk
其中key.snk 代表将保存密钥的文件的名称。它的名称可以是任意的,不过习惯上带有.snk后缀名。
2)签名
这个文件必须在AssemblyKeyFile属性中引用,签名通常是在编译时进行的。签名时,用户可利用C#属性通知编译器应该使用正确的密钥文件对DLL进行签名。要做到这一点用户需要打开工程中的AssemblyInfo.cs文件并进行修改。
[assembly:AssemblyKeyFile(“..\\..\\key.snk”)]
注 意 |
key.snk文件和项目文件在同一个文件夹内。 |
2.手动事务处理
创建一个项目用以实现事务处理的业务类ClassTran。
(示例位置:光盘\code\ch05\04\ClassTran\OrderData1)
using System;
using System.Data.SqlClient;
using System.EnterpriseServices; //企业级服务COM+事务
namespace ClassTran
{
[Transaction(TransactionOption.Required)]
public class OrderData1 : ServicedComponent
{
//手动事务
public string WorkTran()
{
try
{
ContextUtil.EnableCommit();
Work1();
Work2();
ContextUtil.SetComplete();
return "成功!";
}
catch (Exception ex)
{
ContextUtil.SetAbort();
return "失败!";
}
}
private void Work1()
{
string conString = "data source=127.0.0.1;database=codematic;
user id=sa;password=";
SqlConnection myConnection = new SqlConnection(conString);
string strSql = "Insert Into P_Category(CategoryId,Name)
values('1','test1')";
SqlCommand myCommand = new SqlCommand(strSql, myConnection);
myConnection.Open();
int rows = myCommand.ExecuteNonQuery();
myConnection.Close();
}
private void Work2()
{
string conString = "data source=127.0.0.1;database=codematic;
user id=sa;password=";
SqlConnection myConnection = new SqlConnection(conString);
string strSql = "Insert Into P_Category(CategoryId,Name)
values('2','test2')";
SqlCommand myCommand = new SqlCommand(strSql, myConnection);
myConnection.Open();
int rows = myCommand.ExecuteNonQuery();
myConnection.Close();
}
}
}
3.自动事务处理
在方法之前增加属性[AutoComplete(true)],这样如果方法执行时没有异常就默认提交,如果有异常则这个方法就会回滚。
(示例位置:光盘\code\ch05\04\ClassTran\OrderData2)
using System;
using System.Data.SqlClient;
using System.EnterpriseServices;//企业级服务COM+事务
namespace ClassTran
{
[Transaction(TransactionOption.Required)]
public class OrderData2 : ServicedComponent
{
//自动事务
[AutoComplete(true)]
public string WorkTran()
{
string msg = "";
string conString = "data source=127.0.0.1;database=codematic;
user id=sa;password=";
SqlConnection myConnection = new SqlConnection(conString);
myConnection.Open();
SqlCommand myCommand = new SqlCommand();
myCommand.Connection = myConnection;
try
{
myCommand.CommandText = "update P_Product set Name='电脑2'
where Id=52";
myCommand.ExecuteNonQuery();
myCommand.CommandText = "update P_Product set Name='电脑3'
where Id=53";
myCommand.ExecuteNonQuery();
msg ="成功!";
}
catch (Exception ex)
{
msg = "失败:"+ex.Message;
}
finally
{
myConnection.Close();
}
return msg;
}
}
}
4.事务方法调用
(示例位置:光盘\code\ch05\04\Web\WebForm4.aspx)
protected void Button1_Click(object sender, EventArgs e)
{
ClassTran.OrderData1 od1 = new ClassTran.OrderData1();
od1.WorkTran();
}
protected void Button2_Click(object sender, EventArgs e)
{
ClassTran.OrderData2 od2 = new ClassTran.OrderData2();
od2.WorkTran();
}
在需要事务跨 MSMQ 和其他可识别事务的资源(例如SQL Server 数据库)运行的系统中,只能使用 DTC 或 COM+ 事务,除此之外没有其他选择。DTC 协调参与分布式事务的所有资源管理器,也管理与事务相关的操作。
企业级服务COM+事务的前提及优缺点如下。
前提:
l 需要强名字。
l 使用事务的对象需要继承ServicedComponent。
优势:
l 执行分布式事务,多个对象可以轻松地运行在同一个事务处理中,事务处理还可以自动登记。
l 获得COM+服务,诸如对象构建和对象池等。
缺点:
l 由于存在 DTC 和 COM 互操作性开销,导致性能降低。
l COM+ 1.0要求每个事务的隔离级别都设置为Serializable。
l 使用Enterprise Services的事务总是线程安全的, 也就是说你无法让多个线程参与到同一个事务中。
5.4.5 System.Transactions 事务处理
在 .NET Framework 2.0中增加了System.Transactions,这是一种新的命名空间,完全专注于控制事务性行为。引入了执行事务性工作的更简单方法及一些新的性能优化。System.Transactions提供了一个“轻量级”的、易于使用的Transaction框架。
在上节中,要实现Transaction需要利用EnterpriseServices,让组件从ServiceComponent继承下来。而通过System.Transactions,则只要简单的几行代码,不需要继承,不需要Attribute标记。用户根本不需要考虑是简单事务还是分布式事务。新模型会自动根据事务中涉及的对象资源判断使用何种事务管理器。简而言之,对于任何的事务,用户只要使用同一种方法进行处理即可。
下面介绍System.Transactions的几种用法。
首先要引用:using System.Transactions;。
其次,将事务操作代码放在TransactionScope中执行。如:
using (TransactionScope ts = new TransactionScope())
{
//事务操作代码
ts.Complete();
}
这是最简单,也是最常见的用法。创建了新的 TransactionScope 对象后,即开始创建事务范围。如代码示例所示,建议使用 using 语句创建范围。位于 using 块内的所有操作将成为一个事务的一部分,因为它们共享其所定义的事务执行上下文。本例中的最后一行,调用 TransactionScope 的 Complete 方法,将导致退出该块时请求提交该事务。此方法还提供了内置的错误处理,出现异常时会终止事务。
(示例位置:光盘\code\ch05\04\ClassTran\OrderData3)
using (TransactionScope ts = new TransactionScope())//使整个代码块成为事务性代码
{
#region 在这里编写需要具备Transaction的代码
string msg = "";
string conString = "data source=127.0.0.1;database=codematic;user id=sa;
password=";
SqlConnection myConnection = new SqlConnection(conString);
myConnection.Open();
SqlCommand myCommand = new SqlCommand();
myCommand.Connection = myConnection;
try
{
myCommand.CommandText = "update P_Product set Name='电脑2' where Id=52";
myCommand.ExecuteNonQuery();
myCommand.CommandText = "update P_Product set Name='电脑3' where Id=53";
myCommand.ExecuteNonQuery();
msg = "成功!";
}
catch (Exception ex)
{
msg = "失败:" + ex.Message;
}
finally
{
myConnection.Close();
}
#endregion
ts.Complete();
return msg;
}
上面的代码演示了在一个Transaction Scope里面打开一个数据库连接的过程。这个数据库连接由于处在一个Transaction Scope里面,所以会自动获得Transaction的能力。如果这里数据库连接的是SQL Server 2005,那么这个Transaction将不会激活一个MSDTC管理的分布式事务,而是会由.NET创建一个Local Transaction,性能非常高。但是如果是SQL Server 2000,则会自动激活一个分布式事务,在性能上会受一定的损失。
再看下面的例子:
void MethodMoreConn()
{
using (TransactionScope ts = new TransactionScope())
{
using (SqlConnection conn = new SqlConnection(conString1))
{
conn.Open();
using (SqlConnection conn2 = new SqlConnection(conString2))
{
conn2.Open();
}
}
ts.Complete();
}
}
这个例子更加充分地说明了Transaction Scope的强大,两个数据库连接!虽然上面的conn和conn2是两个不同的连接对象,可能分别连接到不同的数据库,但是由于它们处在一个TransactionScope中,它们就具备了“联动”的Transaction能力。在这里,将自动激活一个MSDTC管理的分布式事务(可以通过打开【管理工具】里面的组件服务,来查看当前的分布式事务列表)。
1.在分布式事务中登记
ADO.NET 2.0 中的新增功能支持使用 EnlistTransaction 方法在分布式事务中登记。由于 EnlistTransaction 在 Transaction 实例中登记连接,因此,该方法利用 System.Transactions 命名空间中的可用功能来管理分布式事务,从而比使用 System.EnterpriseServices. ITransaction 对象的 EnlistDistributedTransaction 更可取。此外,其语义也稍有不同:在一个事务中显式登记了某个连接后,如果第一个事务尚未完成,则无法取消登记或在另一个事务中登记该连接。
void MethodEnlist()
{
CommittableTransaction tx = new CommittableTransaction();
using (SqlConnection conn = new SqlConnection(conString))
{
conn.EnlistTransaction(tx);
}
tx.Commit();
}
2.实现嵌套事务范围
void RootMethod()
{
using (TransactionScope scope = new TransactionScope())
{
//操作代码
SonMethod();//子事务方法
scope.Complete();
}
}
void SonMethod()
{
using (TransactionScope scope = new TransactionScope())
{
//操作代码
scope.Complete();
}
}
3.事务范围附加选项
如果你想要保留代码部分执行的操作,并且在操作失败的情况下不希望中止环境事务,则Suppress对你很有帮助。例如,在你想要执行日志记录或审核操作时,不管你的环境事务是提交还是中止,上述值都很有用。该值允许你在事务范围内具有非事务性的代码部分,如以下示例所示。
void MethodSuppress()
{
using (TransactionScope scope1 = new TransactionScope())//开始事务
{
try
{
//开始一个非事务范围
using (TransactionScope scope2 = new TransactionScope(
TransactionScopeOption.Suppress))
{
//不受事务控制代码
}
//从这里开始又回归事务处理
}
catch
{ }
}
}
虽然.NET 2.0对事务提供了很好的支持,但是没有必要总是使用事务。使用事务的第一条规则是,在能够使用事务的时候都应该使用事务,但是不要使用过度。原因在于,每次使用事务都会占用一定的开销。另外,事务可能会锁定一些表的行。还有一条规则是,只有当操作需要的时候才使用事务。例如,如果只是从数据库中查询一些记录,或者执行单个查询,则在大部分时候都不需要使用显式事务。
开发人员应该在头脑中始终保持一个概念,就是用于修改多个不同表数据的冗长事务会严重妨碍系统中的所有其他用户。这很可能导致一些性能问题。当实现一个事务时,遵循下面的实践经验能够达到可接受的结果:
l 避免使用在事务中的Select返回数据,除非语句依赖于返回数据。
l 如果使用Select语句,则只选择需要的行,这样不会锁定过多的资源,而尽可能地提高性能。
l 尽量将事务全部写在T-SQL或者API中。
l 避免事务与多重独立的批处理工作结合,应该将这些批处理放置在单独的事务中。
l 尽可能避免大量更新。
另外,必须注意的一点就是事务的默认行为。在默认情况下,如果没有显式地提交事务,则事务会回滚。虽然默认行为允许事务的回滚,但是显式回滚方法总是一个良好的编程习惯。这不仅仅只是释放锁定数据,也将使得代码更容易读取并且更少错误。
.NET提供的事务功能很强大,具体的内容远不止本文所讲解的这样简单。本文只是起到一个抛砖引玉的功能。希望读者能够灵活恰当地使用事务功能,而不要过度使用事务,否则可能会对性能起到消极的作用。