1. 事务的概念
为了理解.NET对事务的支持,很重要的是建立对事务的整体理解。事务能够确保实现,除非所有操作都成功完成,否则面对数据的资源不会持久化更新。事务由一组要么成功要么失败的操作定义。也就说,如果事务内的所有操作都成功完成,那么提交事务,同时持久化写入更新数据。然而,如果其中一个操作失败,则执行回滚,结果数据回到事务启动前的状态。举例而言,假设需要把100元从帐户A转到帐户B。该操作包括两个步骤:(1)从帐户A中扣除100元。(2)向帐户B添加100元。在发生成功完成步骤1,但是由于一些原因导致步骤2失败的情况下。如果不撤消还原步骤1,那么整个操作将发生错误。事务能够帮助以避免这种情况。如果所有步骤都执行成功,那么在相同事务中的操作将会修改数据库。在本例中,如果步骤2失败,则步骤导致的变化将不会提交给数据库。
通常,事务遵循特定的规则,其称为ACID特性。ACID特性确保复杂事务是自包含和可信赖的。下面简单介绍一下这个特性。
事务必须具有原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。将这四个特性的首字母组合起来就是ACID。虽然首字母缩写词容易记忆,但是每个词的含义不是很明显。以下是简要说明:
原子性:原子性可确保要么执行所有更新,要么什么也不发生。由于事务中的原子性保障,开发人员不必编写代码来处理一个更新成功,而另一个没有成功的情况。
一致性:一致性意味着事务的结果使得系统保持一致状态。在事务启动之前,数据保持有效的状态,这与事务结束时一样。一致性还确保了事务必须使得数据库保持一致状态,那么如果事务的部分操作失败,则其他部分也必须回到原来的状态。
隔离性:多个用户可能同时访问同一个数据库。使用隔离性能够保证在事务完成之前,该事务外部不能看到事务中的数据改变。也不能访问一些中间状态,如果事务终止这些状态将不会发生。
持久性:持久性意味着即使是系统崩溃也能够保证一致性状态。如果数据库系统崩溃,则持久性必须保证已经提交的事务确实写入了数据库。
2. 数据库事务
在很多商业应用程序中经常使用事务,因为事务为系统带来了稳定性和可预测性。通常而言,当开发软件系统时,使用数据源存储数据。为了在这样的软件系统中应用事务的概念,数据源必须支持事务。现代数据库,例如Microsoft SQL Server 2005和Oracle 9i都大力支持事务。例如,SQL Server 2005提供了一些支持事务的T-SQL语句,例如BEGIN TRANSACTION、SAVE TRANSACTION、COMMIT TRANSACTION和ROLLBACK TRANSACTION。数据访问API,例如ODBC,OLE DB和ADO.NET,可使开发人员在应用程序中使用事务。通常,只要使用单个数据库,RDBMS和数据访问API都提供对事务的支持。在很多包括多个数据库的大型应用程序中,可能需要使用Microsoft分布式事务处理协调器(MSDTC)。COM+是一种流行的中间件产品,其在内部利用MSDTC来帮助实现多数据库事务,甚至是不同已知事务实体之间的事务,而通常将其作为资源管理器。应该注意的是,在.NET 2.0中,可以使用System.Transactions命名空间来设置分布式事务,以替代System.EnterpriseServices。
事务分为本地事务和分布式事务两种类型。(1)本地事务:该类型事务使用已知数据源(例如SQL Server),同时还是单阶段事务。当单个数据库中保存了所有有关事务的数据,那么对自身可以强制使用ACID规则。这意味着在单个数据库服务器中(例如SQL Server),只要使用同一个连接,则可以跨数据库使用本地事务。(2)分布式事务:该类型事务使用多个已知事务数据源。分布式行为可能需要从消息队列服务器中读取消息,从SQL Server数据库中获取数据,以及将消息写入其他数据库。
一些软件包(例如MSDTC)能够以编程方式辅助实现分布式事务,通过使用一些方法(例如两阶段提交和回滚)能够控制跨越所有数据源的提交和回滚行为,以便保证集成性。MSDTC仅可用于兼容事务管理接口的应用程序。当前可用的应用程序有MSMQ、SQL Server、Oracle、Sybase和其他当前可用的应用程序(称为资源管理器)。
在分布式事务环境中,不同的资源管理器需要实现可靠的提交协议,最为常见的实现是两阶段提交。在两阶段提交中,实际的提交工作分为两个阶段:第一个阶段包括为提交准备一些所需的更改。这样,资源管理器就会与事务协调器通信,告知其更新准备已经就绪,准备执行提交,但实际还不进行提交。在第二个阶段中,一旦所有资源管理器都告知事务协调器准备工作就绪,那么事务协调器将使所有参与者都了解继续工作准备好,接着执行更改。在两阶段提交中,单个或者多个数据库能够参与分布式事务。实际上,任何在MSDTC事务中登记的对象都能够参与由MSDTC管理的分布式事务。例如,MSMQ能够参与由两个 SqlConnection对象连接两个不同数据库的事务。
3. .NET 2.0中的事务
通过登记正在执行的事务性工作类型的资源管理器,.NET Framework 2.0中的事务管理系统能够解决动态事务组合而导致的额外开销问题。它还提供了将多个不稳定资源转换为提交和回滚事务模型所需的架构。在下面的内容中将介绍.NET 2.0中与事务有关的轻量级事务管理、显式事务、TransactionScope类和自动化事务。
轻量级事务管理
对于发生在单个应用程序域中的事务,LTM是一种运行很快,非常便宜的资源管理器。LTM是框架中所有事务的起点,同时监视正在与事务交互的资源,以及根据需要登记更多健壮的事务管理器的服务。
当事务性工作在进程外工作(例如开始修改数据库数据)时,LTM将自动使用支持可升级的单阶段登记(PSPE)的资源管理器模型。这是一种新的事务性架构,其理解LTM的“预付费”机制。如果没有可用的PSPE管理器,那么LTM会登记DTC。当然,多个远程数据源将被登记的DTC修改。当PSPE模型开始工作时,事务的执行将于ADO.NET 1.x的事务一致。读者可能会怀疑PSPE具有与ADO.NET事务一样的性能。当与多个数据库交互时,事务将自动提升到DTC。
在.NET Framework 2.0中,使用SQL Server 2005时将自动获得PSPE事务。如果事务性工作与另一个服务器或者数据库交互,那么自动使用DTC。易失性事务自动参与PSPE,而不用调用DTC。
实现显式事务
有时候,TransactionScope对象的默认隐式自动事务功能可能无法提供所需较好的控制级别。在这种情况下,可能需要人工创建事务,同时显式提交或者回滚事务。示例1显示使用CommittableTransaction类创建显式事务包括的步骤。
例1:使用CommittableTransaction实现显式事务
<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<%@ Import Namespace="System.Transactions" %>
<%@ Import Namespace="System.Web.Configuration" %>
<script runat="server">
void btnSave_Click(object sender, EventArgs e)
{
CommittableTransaction trans = new CommittableTransaction();
try
{
string connectionString = WebConfigurationManager.ConnectionStrings
["Mydatabase"].ConnectionString;
using (SqlConnection connection = new SqlConnection(connectionString))
{
string sql = "Insert into Production.ProductCategory(Name," +
"rowguid, ModifiedDate) Values(@Name, @rowguid, @ModifiedDate)";
// 打开连接,在事务范围中登记此连接
connection.Open();
SqlCommand command = new SqlCommand(sql, connection);
command.CommandType = CommandType.Text;
SqlParameter nameParam =
new SqlParameter("@Name", SqlDbType.NVarChar, 50);
nameParam.Value = txtCategoryName.Text;
command.Parameters.Add(nameParam);
SqlParameter guidParam = new SqlParameter("@rowguid",
SqlDbType.UniqueIdentifier);
guidParam.Value = System.Guid.NewGuid();
command.Parameters.Add(guidParam);
SqlParameter modifieDateParam = new SqlParameter("@ModifiedDate",
SqlDbType.DateTime);
modifieDateParam.Value = System.DateTime.Now;
command.Parameters.Add(modifieDateParam);
//在当前事务的范围中登记事务
connection.EnlistTransaction(trans);
command.ExecuteNonQuery();
// 如果每一个执行都成功,则提交事务
trans.Commit();
}
lblResult.Text = "Category is written successfully";
}
catch (Exception ex)
{
// 如果出现异常,则回滚事务
trans.Rollback();
lblResult.Text = "Exception is : " + ex.Message;
}
}
</script>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Using Explicit Transactions using CommittableTransaction</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:Label ID="lblCategoryName" runat="server"
Text="Category Name:" Width="179px"></asp:Label>
<asp:TextBox ID="txtCategoryName" runat="server" />
<asp:Button ID="btnSave" runat="server" Text="Save" Width="92px"
OnClick="btnSave_Click" />
<br /><br />
<asp:Label ID="lblResult" runat="server" Font-Bold="true"
Font-Size="Small" />
</div>
</form>
</body>
</html>
在这种方法中,需要调用SqlConnection对象的EnlistTransaction()方法(传递 CommittableTransaction对象作为参数),以便将SqlConnection对象与CommittableTransaction对象关联起来。一旦完成这个工作,然后就可以通过调用CommittableTransaction对象的Commit()和Rollback()方法,显式提交或者回滚事务。正如能够想象的,不推荐使用这种手动方法,因为当发生不同类型的异常时,可能会遇到一些无法回滚事务的风险。
使用TransactionScope类
正如名称所暗示,TransactionScope类用于限定事务代码块,其具有一些明显优点,例如范围与应用程序对象模型无关,同时提供了一个简单直观的编程模型等等。在该类的构造函数内部,TransactionScope对象创建了一个事务(.NET 2.0中默认时轻量级事务管理器),同时将该事务设置给Transaction类的Current属性。由于TransactionScope是可释放对象,所以事务将调用Dispose()方法释放该对象:
using(TransactionScope scope = new TransactionScope())
{
/*在这里实现事务性工作 */
// 没有错误——提交事务
scope.Complete();
}
示例2列举了一种在.NET 2.0中创建事务的方法。在TransactionScope对象定义的代码块中创建和释放该对象。使用TransactionScope对象的构造函数和TransactionScopeOption枚举,开发人员能够定义是否需要新事务,或者是否应该使用已经在外部块中存在的事务。 TransactionScope.Complete()方法指示事务范围内的所有操作都已成功完成。在using语句结尾处(调用Dispose()方法的位置),定义了事务块的输出。如果由于发生异常而没有调用Complete()方法,那么放弃事务。如果在事务范围内成功完成,则如果事务是根事务,那么当事务是根事务时就提交事务。如果范围内的不是根事务,那么会影响事务输出。
例2:使用TransactionScope实现隐式事务
<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<%@ Import Namespace="System.Transactions" %>
<%@ Import Namespace="System.Web.Configuration" %>
<script runat="server">
void btnSave_Click(object sender, EventArgs e)
{
try
{
int categoryID;
string connectionString = WebConfigurationManager.ConnectionStrings
["Mydatabase"].ConnectionString;
using (TransactionScope scope = new TransactionScope())
{
using (SqlConnection connection = new SqlConnection(connectionString))
{
categoryID = InsertCategory(connection);
}
// 提交事务
scope.Complete();
}
lblResult.Text =
"Category is written successfully*****Category ID= " +
categoryID.ToString();
}
catch (Exception ex)
{
lblResult.Text = "Exception is : " + ex.Message;
}
}
int InsertCategory(SqlConnection connection)
{
}
</script>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Implicit Transactions using TransactionScope</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:Label ID="lblCategoryName" runat="server" Text="Category Name:"
Width="179px"></asp:Label>
<asp:TextBox ID="txtCategoryName" runat="server" />
<asp:Button ID="btnSave" runat="server" Text="Save" Width="92px"
OnClick="btnSave_Click" />
<br />
<br />
<asp:Label ID="lblResult" runat="server" Font-Bold="true"
Font-Size="Small" />
</div>
</form>
</body>
</html>
在示例2中,对于Mydatabase数据库执行插入的SQL语句包括在使用using块的TransactionScope对象中。 InsertCategroy()方法执行实际的向ProductCategory表插入新记录的工作。在插入记录后,该方法向调用者返回新近插入记录的标识值(类别ID列)。一旦代码成功执行,则调用TransactionScope对象的Complete()方法,以便告知.NET Framework语句已经成功执行完成,事务导致的结果将提交给数据库。
以下是TransactionScope所完成的一些内容:
出现在using语句括号中的任何语句将在事务范围内执行。
任何在块中创建的连接将在事务中登记。
如果在using块中发生错误,则事务将自动回滚。
如果语句成功执行,那么作为工作的一部分,需要在事务中调用Complete()方法。
调用堆栈的每一步必须调用Complete(),以便提交事务。
TransactionScope对象无法了解是否应该提交或者放弃事务,TransactionScope的主要目标是避免开发人员与事务直接交互。为了解决这个问题,每个TransactionScope对象都有一个一致性位,其默认设置为false。通过调用Complete()方法能够将一致性位设置为true。注意,只能调用一次Complete()。后续对Complete()的调用将引发InvalidOperation异常,因为在调用 Complete()之后,不能保证还有事务性代码。
ASP.NET中的自动化事务
通过在ASP.NET页面中添加Transaction属性,可使得ASP.NET能够在系统中支持自动事务。利用Transaction属性,开发人员能够指示页面参与现有事务,开始新事务,或者不参与事务。下表列举了ASP.NET中可用的Transaction属性值。
通过在代码中的Page指令中设置Transaction属性能够定义页面支持的事务级别。例如,插入以下指令能够保证页面活动总是在事务范围中执行:
<%@ Page Transaction="Required" %>
如果省略Transaction属性,页面则禁用事务。使用System.EnterpriseServices.ContextUtil类的静态方法在 ASP.NET页面中提交或者放弃事务。这些静态方法是SetComplete()和SetAbort()(它们分别对应Page事件 CommitTransaction()和AbortTransaction())。以下代码列举了页面实现框架,该页面将Page指令的 Transaction属性设置为Required,同时在CommitTransaction()和AbortTransaction()事件中,编写处理事务结果所需的代码。
void Page_Load(object sender, System.EventArgs e)
{
AbortTransaction += new System.EventHandler(AbortTransactionEvent);
CommitTransaction += new System.EventHandler(CommitTransactionEvent);
try
{
/* 在这里放置事务性代码 */
ContextUtil.SetComplete();
}
catch(Exception)
{
ContextUtil.SetAbort();
}
}
void AbortTransactionEvent(object sender,System.EventArgs e)
{
/*用于回滚行为的代码*/
}
void CommitTransactionEvent(object sender,System.EventArgs e)
{
/*用于提交行为的代码*/
}
4. 何时使用事务
虽然.NET 2.0对事务提供了很好的支持,但是没有必要总是使用事务。使用事务的第一条规则是,在能够使用事务的时候都应该使用事务,但是不要使用过度。原因在于,每次使用事务,都会占用一定的开销。另外,事务可能会锁定一些表的行。还有一条规则是,只有当操作需要的时候才使用事务。例如,如果只是从数据库中查询一些记录,或者执行单个查询,在大部分时候都不需要使用显式事务。
开发人员应该在头脑中始终保持一个概念,就是用于修改多个不同表数据的冗长事务会严重妨碍系统中的所有其他用户。这很可能导致一些性能问题。当实现一个事务时,遵循下面的实践经验能够达到可接受的结果:(1)避免使用在事务中的SELECT返回数据,除非语句依赖于返回数据;(2)如果使用SELECT语句,只选择需要的行,这样不会锁定过多的资源,而尽可能的提高性能;(3)尽量将事务全部写在T-SQL或者API中;(4)避免事务与多重独立的批处理工作结合,应该将这些批处理放置在单独的事务中;(5)尽可能避免大量更新。
另外,必须注意的一点就是事务的默认行为。在默认情况下,如果没有显式的提交事务,则事务会回滚。虽然默认行为允许事务的回滚,但是显式回滚方法总是一个良好的编程习惯。这不仅仅只是释放锁定数据,也将使得代码更容易读并且更少错误。
5. 小结
.NET 2.0提供的事务功能很强大,具体的内容远不止本文所讲解的这样简单。本文只是起到一个抛砖引玉的功能。希望读者能够灵活恰当的使用事务功能,而不要过去使用事务,否则可能会对性能起到消极的作用