ASP.NET 2.0数据库高级编程(事务概述 )

来源:互联网 发布:bootstrap获取tr数据 编辑:程序博客网 时间:2024/05/06 11:09

事实上, 所有的商业应用程序都需要不同层次的事务支持。利用关系型数据库提供的架构规则,在很大程度上能够在静态视图中显示完整数据。然而,在动态过程中,事务能够在持久化过程结束时,确保应用或者不应用所有的变化。ACID属性(原子性atomicity、一致性consistency、隔离性isolation和持久性durability)是任何事务架构的基础。通常,事务与数据库交互相关,同时通常认为无论何时讨论事务,都由数据库来实现。然而,事务化行为可用于任何资源,例如内存中的哈希表,硬盘上的文件或者XML文档。内建于.NET Framework 2.0中的事务引擎的设计目标之一是,更容易为参与事务的任何类型资源创建资源管理器。本章将首先介绍事务,然后讲解.NET 1.x和2.0中的事务功能。具体而言,本章内容包括:

  ●    不同的事务类型

  ●    .NET 1.x的事务在连接和非连接模式下的工作机制

  ●    在.NET 1.x中如何使用Transaction类管理事务

  ●    如何使用企业级服务管理分布式事务

  ●    如何不通过服务组件使用事务

  ●    如何使用System.Transactions命名空间中的类

  ●    如何使用TransactionScope类创建隐式事务

  ●    如何创建可升级事务

  ●    如何使用TransactionOptions配置事务设置

  ●    如何创建显式事务

  ●    如何处理事务事件

  ●    System.EnterpriseServices和System.Transactions之间的交互

11.1  事务概述

要理解.NET对事务的支持,很重要的是建立对事务的整体理解。事务能够确保除非所有操作都成

功完成,否则面对数据的资源不会持久化更新。事务由一组要么成功要么失败的操作定义。如果事务内的所有操作都成功完成,那么提交事务,同时持久化写入更新数据。然而,如果其中一个操作失败,则执行回滚,结果数据回到事务启动前的状态。假设需要把100美元从账户A转到账户B。该操作包括两个步骤:

① 从账户A中扣除100美元。

② 向账户B添加100美元。

成功完成步骤1,但是由于一些原因导致步骤2失败。如果不撤销还原步骤1,那么整个操作将发生错误。事务能够帮助避免这种情况发生。如果所有步骤都执行成功,在相同事务中的操作将会修改数据库。在本示例中,如果步骤2失败,步骤导致的变化将不会提交给数据库。

通常,事务遵循特定的规则称为ACID属性。ACID属性确保复杂事务是自包含和可信赖的。

11.1.1  ACID属性

为了通过ACID测试,事务必须具有原子性、一致性、隔离性和持久性。虽然首字母缩写词容易记忆,但是每个词的含义不是很明显。以下是简要说明。

  ●    原子性(Atomicity):原子性可确保要么执行所有更新,要么什么也不发生。由于事务中的原子性保障,开发人员不必编写代码来处理更新成功而另一个没有成功的情况。

  ●    一致性(Consistency):一致性意味着事务的结果使得系统保持一致状态。在事务启动之前,数据保持有效的状态,这与事务结束时一样。一致性还确保了事务必须使得数据库保持一致状态,如果事务的部分操作失败,则其他部分也必须回到原来的状态。

  ●    隔离性(Isolation):多个用户可能同时访问同一个数据库。使用隔离性能够保证在事务完成之前,该事务外部不能看到事务中的数据改变。也不能访问一些中间状态,如果事务终止这些状态将不会发生。

  ●    持久性(Durability):持久性意味着即使是系统崩溃也能够保证一致性状态。如果数据库系统崩溃,则持久性必须保证已经提交的事务确实写入了数据库。

                                                                                                                                     11.1  事务概述

11.1.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 1.1提供了利用System.EnterpriseServices命名空间访问COM+的功能;在.NET 2.0中,可以使用System.Transactions命名空间来设置分布式事务,以替代System.EnterpriseServices。

事务类型

事务分为本地事务和分布式事务两种类型。

  ●    本地事务:该类型事务使用已知数据源(例如SQL Server),同时还是单阶段事务。若单个数据库中保存了所有有关事务的数据,对自身可以强制使用ACID规则。这意味着在单个数据库服务器中(例如SQL Server),只要使用同一个连接,则可以跨数据库使用本地事务。

  ●    分布式事务:该类型事务使用多个已知事务数据源。分布式行为可能需要从消息队列服务器中读取消息,从SQL Server数据库中获取数据,以及将消息写入其他数据库。

一些软件包(例如MSDTC)能够以编程方式辅助实现分布式事务,通过使用一些方法(例如两阶段提交和回滚)能够控制跨越所有数据源的提交和回滚行为,以便保证集成性。MSDTC仅可用于兼容事务管理接口的应用程序。当前可用的应用程序有MSMQ、SQL Server、Oracle、Sybase和其他当前可用的应用程序(称为资源管理器)。

两阶段提交

在分布式事务环境中,不同的资源管理器需要实现可靠的提交协议,最为常见的实现是两阶段提交。在两阶段提交中,实际的提交工作分为两个阶段:

  ●    第一个阶段包括为提交准备一些所需的更改。这样,RM(资源管理器)就会与事务协调器通信,告知其更新准备已经就绪,准备执行提交,但实际还不进行提交。

  ●    一旦所有资源管理器都告知事务协调器准备工作就绪,那么事务协调器将使所有参与者都了解继续工作准备好,接着执行更改。

在两阶段提交中,单个或者多个数据库能够参与分布式事务。实际上,任何在MSDTC事务中登记的对象都能够参与由MSDTC管理的分布式事务。例如,MSMQ能够参与由两个SqlConnection对象连接两个不同数据库的事务。简单描述两阶段提交显然过于简单化,而深入讲解两阶段提交又超出了本书范围。既然读者对事务有了初步认识,就能够理解.NET 1.x提供的针对事务过程的支持。

11.1.3  .NET 1.x中的事务

ADO.NET 1.x对于数据库事务提供了强大支持。ADO.NET自身支持单个数据库事务,它根据每个连接实现跟踪。ADO.NET的连接对象提供了事务功能。ADO.NET连接对象只用于启动事务。利用实现Transaction类的专用对象能够实现事务的提交和回滚。这样能够将不同的命令对象与单个事务对象关联起来,因此这些命令对象参与到了同一个事务中。

ADO.NET提供了连接和非连接的数据访问,还提供了在这两种模式下对事务的支持。在连接模式下,常见的事务执行步骤如下所示:

① 打开数据库连接。

② 启动事务。

③ 通过命令对象直接触发对连接对象的查询。

④ 提交或者回滚事务。

⑤ 关闭连接。

图11-1显示了在连接模式下事务的处理机制。

图11-1

通常,在非连接模式下,首先将数据(通常为一个或者多个表)读取到DataSet对象中,接着关闭数据源连接,然后根据需要操作数据,最后更新数据库中的数据。在这种模式下,通常的操作顺序如下所示:

① 打开数据库连接。

② 将所需数据读取到DataSet对象中。

③ 关闭数据库连接。

④ 在DataSet对象中操作数据。

⑤ 再次打开数据库连接。

⑥ 启动事务。

⑦ 将事务对象赋值给相关数据适配器中的命令对象。

⑧ 按照DataSet的改变更新数据库。

⑨ 关闭连接。

图11-2说明了事件顺序。

在连接模式下实现事务相对简单,因为每个事件都是即时发生的。然而,在非连接模式下,当使用数据更新数据库时,应该注意并发问题。同时,根据架构,可能有必要回滚DataSet导致的更改,即使这些更改在回滚更新过程中可能已经部分完成了。

读者已经理解了ADO.NET的连接和非连接事务处理架构,以下章节将讨论事务类。还将了解事务类的常用方法和使用这些方法的典型方式。

图11-2

1.Transaction类

.NET Framework包括一些可用的.NET受管提供程序,例如OleDb、SqlClient、OracleClient、ODBC等,它们都实现了自身的事务类:OleDb数据提供程序实现了OleDbTransaction类,该类包括在Systm.DataOdbc命名空间中。其他提供程序与OleDb类似(SqlTransaction类和OracleTransaction类)。

所有这些类都实现了System.Data命名空间中的IdbTransaction接口。这些类的多数属性和方法都类似,然而,每个类都有一些自身特定的方法,下面的章节将讨论这些方法。

Transaction类的方法

事务类中有两个经常使用的方法。

  ●    Commit:该方法表示成功完成事务。一旦调用这个方法,且该方法没有返回错误,则所有挂起更改都将写入底层数据库。具体实现依靠数据提供程序,但是通常都转换为在底层数据库执行COMMIT语句。

  ●    Rollback:该方法表示未成功实现事务,同时删除挂起更改。数据库状态保持不变。

通常而言,这两个方法一起使用。为了执行事务中的命令,必须设置命令对象中的Transaction属性。示例11-1列举了如何显式启动和管理事务的示例。

示例11-1:在.NET 1.x中实现显式事务管理

string connectionString = "...";

IDbConnection connection = new SqlConnection(connectionString);

connection.Open();

IDbCommand command = new SqlCommand();

command.Connection = connection;

IDbTransaction transaction;

transaction = connection.BeginTransaction();// 登记数据库

command.Transaction = transaction;

try

{

  /* 与数据库交互,然后提交事务 */

  transaction.Commit();

}

catch

{

  transaction.Rollback(); // 终止事务

}

finally

{

  connection.Close();

}

通过调用SqlConnection.BeginTransaction()方法,能够返回实现IDbTransaction接口(该接口用于管理事务)的对象。该对象表示底层数据库事务。如果针对数据库的所有更新或者更改是一致的,则最后调用事务对象的Commit()。如果发生错误,则调用Rollback()方法以放弃事务。

虽然显式编程模型很简单,但是不需要执行事务的类。这种方式适用于单个对象或者与单个数据库交互的ASP.NET页面,如图11-3所示。

图11-3

图11-3讨论的应用很简单,其中不需要跨多个源的协调器,因为仅是通过单个对象与单个数据库通信。然而,如果将这个模型扩展为使用多个对象(如图11-4所示)与多个源通信来完成事务(不失去事务的完整性),它会变得非常复杂。一种可能的解决方案是为事务协调器添加逻辑实现对象连接,但是这种方法非常脆弱,无法经受较小的业务流变化,或者多个对象的参与。另外,图11-4中的对象可由不同的人员开发,不需要任何协调器。

正如看到的,当包括多个源时,情况会变得更加复杂。最大的挑战是在单个事务中包括了多个对象,引入多个源的同时也引入了多种失败的可能性。每个源可能都是事务提交失败的方面。这种类型的应用称为分布式事务。分布式事务包括两个或者多个独立的实体或者对象(经常处于不同的执行环境中),或者包括两个或者多个事务源,或者既有多个对象又有多个源。尝试显式管理所有分布式事务的潜在错误是不切实际的。对于分布式事务而言,需要依靠两阶段协议和专用事务管理器。

图11-4

在Windows中,分布式事务协调器(DTC)系统服务使用OLE事务(OleTx)协议管理跨组件,跨过程和跨机器的事务。虽然依赖DTC以编程方式能够实现事务,但是在.NET 1.x中最常见最容易使用DTC事务的方法,是用System.EnterpriseServices命名空间中的企业级服务。下一节讲解企业级服务的使用方法。

2.使用企业级服务实现分布式事务

.NET企业级服务(也称为COM+服务)提供了一个声明性编程模型:任何继承自ServicedCoponent抽象类的类都能够使用Transaction属性。该属性确保当调用类的任何方法时,方法在事务上下文中执行。上下文是内部执行范围的服务组件。.NET获取进入上下文的调用,接着启动代表对象的事务。这里不需要显式登记资源。显然,如果源能够自动参与事务,才能正常工作。这种源称为事务源管理器,许多流行的商业数据库(包括SQL Server,Oracle和DB2等)和持久性源(例如Microsoft消息队列)都是源管理器。所有对象必须告知.NET是否应该提交或者放弃事务。虽然使用ContextUtil辅助类的方法可能实现相关功能,但还是应以声明的方式使用AutoComplete属性来完成。如果在方法中没有抛出异常,AutoComplete属性将提交事务。如果发生异常,则放弃事务:

using System.EnterpriseServices;

[Transaction]

public class Product : ServicedComponent

{

  [AutoComplete]

  public void InsertProduct()

  {

    /*与其他服务组件和源管理器交互*/

  }

}

使用类属性[Transaction]能够设置是否类的对象了解事务,以及是否应该通过企业级服务运行时自动创建事务。

正如在上面代码中看到的,自动事务功能很强大,不必传递事务作为方法的参数。相反,事务自动随同上下文活动。使用事务属性,能够设置是否需要事务。

虽然声明性模型大幅提升了生产效率,但是它存在一些缺点:

  ●    强制继承ServicedComponent占用了基类的位置,该位置通常为内部应用程序模型保留。

  ●    使用企业级事务总是意味着使用分布式DTC事务,甚至在单个源和单个对象的情况下也是如此。由于源必须具有登录这一操作,因此,无论在事务管理器级别还是在源级别中,两阶段提交协议都会有其自身的开销。与显式事务管理相比,这种开销会导致性能降低。

  ●    使用企业级服务意味着使用COM+宿主模型,这让很多开发人员感到害怕。

  ●    企业级服务事务与企业级服务实例管理策略紧紧结合。所有事务对象都即时激活,而当将事务与对象池绑定时可能会出现一些问题。虽然这种结合在可扩展应用程序中很重要,但是对于其他应用程序而言,其强制使用了一种状态感知编程模型,而多数开发人员都难于使用该模型。

  ●    企业级服务事务总是线程安全的,这意味着在同一事务中,无法使用多线程。虽然事务管理很简单,但尤其在多线程环境中,从某种程度上说是受限的。

实际上,.NET 1.x将非分布式事务与显式事务管理相等同,将分布式事务与显式事务、与企业级服务相等同。不存在使用声明性事务而不使用DTC事务的方法,也没有一种简单的使用受管代码的方法来实现利用DTC的显式事务管理。选择编程模型(显式或者声明式)的同时也要选择事务管理器,反之亦然。

3.不通过服务组件使用事务

从Windows Server 2003和Windows XP2开始,不使用服务组件类定义事务属性就能够创建事务的上下文。使用ServiceConfig、ServiceDomain和Activity类(都包括在System. EnterpriseServices命名空间中)能够创建事务的上下文。这对于使用和不使用服务组件都很有用。通过服务组件这项功能,使得类的方法使用事务成为可能,尽管另一个方法不能再使用事务。

示例11-2中的代码说明了不通过服务组件使用事务的方法。在从配置文件中读取连接字符串之后,将创建新的ServiceConfig对象。使用ServiceConfig对象能够配置上下文,同时,使用该对象的Transaction属性能够设置事务属性,可以将Transaction属性值设置为TransactionOption类型。此处,Transaction属性的值设置为TransactionOption.Required。在本示例中,向ProductCategory表中插入了一个类型,同时使用返回的类别ID值,向ProductSubcategory表添加一个子类别。

示例11-2:不通过服务组件使用事务

<%@ Page Language="C#" %>

<%@ Import Namespace="System.Data" %>

<%@ Import Namespace="System.Data.SqlClient" %>

<%@ Import Namespace="System.EnterpriseServices" %>

<%@ Import Namespace="System.Web.Configuration" %>

<script runat="server">

  void btnSave_Click(object sender, EventArgs e)

  {

    int categoryID = 0;

    int subCategoryID = 0;

    string connectionString = WebConfigurationManager.ConnectionStrings

      ["AdventureWorks"].ConnectionString;

    ServiceConfig config = new ServiceConfig();

    config.Transaction = TransactionOption.Required;

    // 开始事务代码

    ServiceDomain.Enter(config);

    // 插入ProductCategory

    using (SqlConnection connection = new SqlConnection(connectionString))

    {

      categoryID = InsertCategory(connection);

    }

    //在同一事务中,同时插入ProductSubCategory

    using (SqlConnection connection = new SqlConnection(connectionString))

    {

      subCategoryID = InsertSubCategory(connection, categoryID);

    }

    lblResult.Text =

      "Category and Subcategory are written successfully---" +

      "  Category ID :" + categoryID.ToString() +

      "  Subcategory ID: " + subCategoryID.ToString();

    // 结束事务代码

    TransactionStatus status = ServiceDomain.Leave();

    Response.Write("Transaction Status : " + status.ToString());

  }

  int InsertCategory(SqlConnection connection)

  {

    string sql = "Insert into Production.ProductCategory(Name, rowguid," +

      " ModifiedDate) Values(@Name, @rowguid, @ModifiedDate);

    SELECT @@IDENTITY";

    // 打开连接,将使连接于事务范围内

    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);

    int categoryID = Convert.ToInt32(command.ExecuteScalar());

    return categoryID;

  }

  int InsertSubCategory(SqlConnection connection, int categoryID)

  {

    string sql =

      "Insert into Production.ProductSubcategory(ProductCategoryID," +

      "Name, rowguid, ModifiedDate) Values(@ProductCategoryID,

      @Name, @rowguid," + "@ModifiedDate); SELECT @@IDENTITY";

    // 打开连接,使连接在事务范围内

    connection.Open();

    SqlCommand command = new SqlCommand(sql, connection);

    command.CommandType = CommandType.Text;

    SqlParameter categoryIDParam =

           new SqlParameter("@ProductCategoryID", SqlDbType.Int);

    categoryIDParam.Value = categoryID;

    command.Parameters.Add(categoryIDParam);

    SqlParameter nameParam =

           new SqlParameter("@Name", SqlDbType.NVarChar, 50);

    nameParam.Value = txtSubCategoryName.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);

    int subCategoryID = Convert.ToInt32(command.ExecuteScalar());

    return subCategoryID;

  }

</script>

<html xmlns="http://www.w3.org/1999/xhtml">

<head id="Head1" runat="server">

  <title>Transactions using Enterprise Services</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" />

      <br /><br /><br />

      <asp:Label ID="lblSubCategoryName" runat="server"

        Text="Subcategory Name" Width="177px"></asp:Label>

      <asp:TextBox ID="txtSubCategoryName" runat="server" />

      &nbsp;<br /><br />

      <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>

根据名称暗示可知,InsertCategory()辅助方法包括将新类别记录插入到ProductCategory表所需的代码,同时,InsertSubCategory()方法利用InsertCategory()方法返回的类别ID,实现向ProductSubcategory表中插入子类别。这些方法在本章的不同示例中使用。

在示例11-2中,一旦将上下文配置信息传递给ServiceDomain.Enter()方法,接着调用该方法就能够创建上下文。ServiceDomain.Leave()方法返回事务的状态,该状态使用TransactionStatus枚举值表示。当上下文调用ServiceDomain.Leave()方法时,则提交事务。TransactionStatus枚举的可能值如表11-1所示。

表11-1

成    员

说    明

Aborted

中止事务。要么数据库返回错误,要么方法生成异常,那么会中止事务

Aborting

正在中止事务

Committed

成功提交事务。通过设置一致性,每个参与到事务中的成员都能够决定事务

LocallyOk

事务未提交,也未中止。如果被遗弃的上下文运行在另一个事务的上下文中,那么返回该状态

NoTransaction

事务没有上下文,则返回NoTransaction

ServiceDomain类使得在自身上下文中运行.NET受管对象成为可能。使用ServiceDomain类的Enter()和Leave()方法能够指示对象运行在自身上下文中。封装在块中的代码能够使用System.EnterpriseServices命名空间公开的功能。开发人员还能够嵌套Enter()和Leave()方法。然而,需要保证的是在调用每个Enter()方法时要有一个对应的Leave()方法。

使用不调用服务组件的事务在一些情况下很有用:

  ●    对于独立应用程序中的事务很有帮助。

  ●    通过使用服务组件的ASP.NET Web服务,能够在不使用服务组件配置的情况下创建根事务上下文。

  ●    在服务组件中无组件的服务也很有用。例如,如果服务组件类有一个需要事务的方法,同时另一个方法却不需要该事务的情况。

图11-5显示了生成的输出。

图11-5

如果在未安装SP2的Windows XP中运行这个示例应用程序,那么将得到PlatformNotSupportedException异常。无组件的服务仅支持Windows Server 2003和Windows XP SP2。

11.1.4  .NET 2.0中的事务

通过登记正在执行的事务性工作类型的资源管理器,.NET Framework 2.0中的事务管理系统能够解决动态事务组合而导致的额外开销问题。它还提供了将多个不稳定资源转换为提交和回滚事务模型所需的架构。为了解决刚刚说明的显式和声明性事务编程模型问题,.NET Framework 2.0引入了两种事务管理器和一个管理命名空间。这两个新的事务管理器是:

  ●    轻量级事务管理器(LTM)—LTM用于管理单个应用程序域中的事务,该应用程序域至多包括一个持久性源。

  ●    分布式OleTx事务管理器—这种类型的事务管理器管理跨应用程序域边界的事务(包括跨过程和跨机器边界),或者是同一应用程序域中包括的多个持久性源事务。OleTx事务管理器依靠RPC实现跨机器调用,等同于使用局部更改的DTC行为。

LTM仅用于内部应用程序域调用,它比OleTx事务管理器性能高。开发人员不需要与事务管理器直接交互。相反,可在System.Transactions命名空间中使用通用架构定义接口、事务工厂、常见行为和辅助类。通过两个称为System.Transactions资源管理器(RM)来实现资源管理。与企业级服务类似,System.Transactions资源管理器是一个能够自动登记由LTM或者OleTx事务管理器管理的事务的资源。

对通用事务管理命名空间(System.Transactions)编程而不用专用事务管理器的主要优点是前者更高级。事务管理器提升是由System.Transactions支持的创新技术。提升的含义很简单:开发人员只需要决定需求的编程模型(显式或者声明性事务管理),同时System.Transactions将正确地设置适当的事务管理器。

如果对象与单个持久性资源交互,则仅需要LTM,而且这显然能生成最佳流量和性能。如果将事务提供给同一台机器中另一个应用程序域的另一个对象,或者登记另一个持久性资源管理器,事务将自动得到提升,以便由OleTx实施管理。一旦事务提升,将在提升后的状态下保持事务管理,直到完成事务。

LTM和OleTx事务管理器都使用称为Transaction的类实现事务,该类在System.Transactions命名空间中定义。Transaction类用于支持事务中的资源、放弃事务、设置隔离级别、获取事务状态和ID,以及复制事务。

1.轻量级事务管理

对于发生在单个应用程序域中的事务,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。

2.使用TransactionScope类

正如名称所暗示的,TransactionScope类用于限定事务代码块。在该类的构造函数内部,TransactionScope对象创建了一个事务(.NET 2.0中默认时轻量级事务管理器),同时将该事务设置给Transaction类的Current属性。由于TransactionScope是可释放对象,所以事务将调用Dispose()方法释放该对象:

using(TransactionScope scope = new TransactionScope())

{

  /*在这里实现事务性工作 */

  // 没有错误—提交事务

  scope.Complete();

}

示例11-3列举了一种在.NET 2.0中创建事务的方法。在TransactionScope对象定义的代码块中创建和释放该对象。使用TransactionScope对象的构造函数和TransactionScopeOption枚举,开发人员能够定义是否需要新事务,或者是否应该使用已经在外部块中存在的事务。TransactionScope.Complete()方法指示事务范围内的所有操作都已成功完成。在using语句结尾处(调用Dispose()方法的位置),定义了事务块的输出。如果由于发生异常而没有调用Complete()方法,则放弃事务。如果在事务范围内成功完成,那么当事务是根事务时就提交事务。如果范围内的不是根事务,那么会影响事务输出。

示例11-3:使用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

        ["AdventureWorks"].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)

  {

    // 与示例11-2中显示的InsertCategory方法相同

  }

</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" />&nbsp;

      <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>

在示例11-3中,对于AdventureWorks数据库执行插入的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()之后,不能保证还有事务性代码。

如果事务结尾(取决于调用Dispose()或者垃圾收集)和一致性位设置为false,则放弃事务。例如,以下范围对象将总是回滚事务,因为一致性位保持其默认值不改变:

using(TransactionScope scope = new TransactionScope())

{

}

另一方面,如果如同示例11-3那样调用Complete()方法,同时设置值为true的一致性位作为事务结尾,则提交事务。

TransactionScope类的优点

与前面的.NET 1.x的事务编程相比,TransactionScope对象具有一些明显优点和好处。它们是:

  ●    事务范围内的代码不仅是事务性的,而且还是可提升的。事务以LTM(轻量级事务管理器)为开始,同时System.Transactions将根据与资源或者远程对象的交互需要提升事务。

  ●    范围与应用程序对象模型无关,这意味着任何代码片段都能够通过使用TransactionScope而成为事务性的。对于专用基类或者属性则没有必要。

  ●    没有必要使用事务显式登记资源。任何System.Transactions资源管理器将检测周边由范围创建的事务,接着进行自动登记。

  ●    该对象提供了一种简单直观的编程模型,甚至可用于包括事务流和事务嵌套的复杂场景。

3.可升级事务

正如前文提到的,System.Transactions命名空间能够快速和容易地管理事务,而无需担心基于服务组件实现而引起的复杂性。System.Transactions中的轻量级事务的重要功能之一是,能够自动确定是否需要将轻量级事务升级为包括MSDTC的分布式事务。轻量级事务还是一种为本地事务使用DTC的快速备选方案。

本节将讲解如何将包括单个SQL连接的轻量级事务简单自动升级为可能包括多个连接的分布式事务。这些可全部在单个事务上下文中实现(使用TransactionScope),而不用继承ServicedComponent。

为了讲解这个内容,本节将在示例11-3的基础上添加另一个方法InsertSubCategory(),该方法能够向ProductSubcategory表中插入一个子类别。这两个方法(InsertCategory和InsertSubCategory)内嵌在同一个TransactionScope块中。

示例11-4:使用可升级事务

<%@ 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

    {

      string connectionString = WebConfigurationManager.ConnectionStrings

        ["AdventureWorks"].ConnectionString;

      int categoryID, subCategoryID;

      using (TransactionScope scope = new TransactionScope())

      {

        // 插入ProductCategory

        using (SqlConnection connection = new SqlConnection(connectionString))

        {

          categoryID = InsertCategory(connection);

        }

        //在相同事务中同时插入ProductSubCategory

        using (SqlConnection connection = new SqlConnection(connectionString))

        {

          subCategoryID = InsertSubCategory(connection, categoryID);

        }

        // 提交事务

        scope.Complete();

      }

      lblResult.Text = "Category and Subcategory are written successfully---"

        + "    Category  ID  :" + categoryID.ToString() +

        "    Subcategory  ID:  " + subCategoryID.ToString();

    }

    catch (Exception ex)

    {

      lblResult.Text = "Exception  is  :  " + ex.Message;

    }

  }

  int InsertCategory(SqlConnection connection)

  {

    // 与示例11-2中展示的InsertCategory方法相同

  }

  int InsertSubCategory(SqlConnection connection, int categoryID)

  {

    // 与示例11-2中展示的InsertSubCategory方法相同

  }

</script>

<html xmlns="http://www.w3.org/1999/xhtml">

<head runat="server">

  <title>Implicit Distributed 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" />

      <br/><br/><br/>

      <asp:Label ID="lblSubCategoryName" runat="server" Text="Subcategory Name"

        Width="177px"></asp:Label>

      <asp:TextBox ID="txtSubCategoryName" runat="server" />

      &nbsp;<br/><br/>

      <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>

</html>

当打开数据库的第一个连接时,将创建轻量级事务。然而,当打开第二个SqlConnection时,事务将自动升级为分布式事务。注意,从轻量级到分布式事务的升级仅发生在SQL Server 2005中。

4.使用TransactionOptions配置事务设置

示例11-3利用的TransactionScope对象使用了默认构造函数。由于这个原因,所以应用默认隔离级别和超时值。事务隔离级别的默认值是System.Transactions.IsolationLevel. Serializable,超时时间是60秒。有时候能够使用自定义值重写这些值,从而对这些设置进行更好的控制。TransactionScope对象提供了多个可接受这些值的重载构造函数。示例11-5列举了如何实现这一功能的示例。

示例11-5:使用TransactionOptions配置事务设置

<%@ 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

        ["AdventureWorks"].ConnectionString;

      TransactionOptions transactionOption = new TransactionOptions();

      transactionOption.IsolationLevel =

        System.Transactions.IsolationLevel.ReadCommitted;

      // 设置事务超时时间为60秒

      transactionOption.Timeout = new TimeSpan(0, 0, 60);

      using (TransactionScope scope = new

        TransactionScope(TransactionScopeOption.Required, transactionOption))

      {

        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)

  {

    // 与示例11-2中展示的InsertCategory方法相同

  }

</script>

<html xmlns="http://www.w3.org/1999/xhtml">

<head runat="server">

  <title>Configuring Settings with 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" />&nbsp;

      <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>

为了设置隔离级别和事务过期时间,创建一个TransactionOptions对象,同时将该对象的IsolationLevel和Timeout属性设置为适当的值。表11-2列举了TransactionIsolationLevel枚举支持的成员。

表11-2

成    员

说    明

Any

如果将隔离级别设置为Any,则使用与调用组件相同的隔离级别。如果对象是根对象,则隔离级别为Serializable

ReadUncommitted

使用ReadUncommitted时,仅使用共享锁定,并允许非独占方式锁定。该选项仅应该用于只读访问,以便不需要必须在事务范围内生成结果

ReadCommitted

使用该选项,当读取数据时使用共享锁定。在读取数据之后,释放共享锁定。在事务结束之前,可以改变数据

RepeatableRead

如果设置为RepeatableRead,则在所有正在使用的数据上放置锁

Serializable

Serializable能够实现最好的隔离。使用该选项,不可能更新或者插入属于范围内的数据

将TransactionOptions对象作为参数传递给TransactionScope对象的构造函数。在示例11-5中,TransacationScope对象的构造函数还将TransactionScopeOption枚举作为参数。表11-3列举了TransactionScopeOption枚举的可能值。

表11-3

成    员

说    明

Required

如果事务在当前活动事务范围,则使用该事务范围。否则,将创建自己的事务范围

RequiresNew

事务将创建自己的事务范围

Suppress

在创建范围时,当前事务上下文被取消。所有在范围内的操作都在无事务上下文的情况下完成

5.控制显式事务

TransactionScope对象的默认隐式自动事务功能可能无法提供所需较好的控制级别。在这种情况下,可能需要人工创建事务,同时显式提交或者回滚事务。示例11-6显示使用CommittableTransaction类创建显式事务包括的步骤。

示例11-6:使用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

        ["AdventureWorks"].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" />&nbsp;

      <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()方法,显式提交或者回滚事务。正如读者能够想象的,不推荐使用这种手动方法,因为当发生不同类型的异常时,可能会遇到一些无法回滚事务的风险。

6.ASP.NET中的自动化事务

通过在ASP.NET页面中添加Transaction属性,使得ASP.NET能够在系统中支持自动事务。通过Transaction属性,开发人员能够指示页面参与现有事务,开始新事务,或者不参与事务。表11-4列举了ASP.NET中可用的Transaction属性值。

表11-4

说    明

Disabled

指示ASP.NET忽略事务上下文。这是默认事务状态

NotSupported

指示页面不运行在事务范围中。当处理请求时,无论是否有活动事务,都会创建无事务的对象上下文

Supported

指示页面运行在现有事务的上下文中。如果不存在事务,则页面不带事务运行

Required

页面运行在现有事务的上下文中。如果不存在事务,页面启动一个新事务

RequiresNew

指示页面需要一个事务,同时为每个请求启动一个新事务

通过在代码中的Page指令中设置Transaction属性能够定义页面支持的事务级别。例如,插入以下指令能够保证页面活动总是在事务范围中执行:

<%@ Page Transaction="Required" %>

如果省略Transaction属性,页面则禁用事务。使用System.EnterpriseServices. ContextUtil类的静态方法在ASP.NET页面中提交或者放弃事务。这些静态方法是SetComplete()和SetAbort()(它们分别对应Page事件CommitTransaction()和AbortTransaction())。以下代码列举了页面实现框架,该页面将Page指令的Transaction属性设置为Required:

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)

{

  /*用于提交行为的代码*/

}

在CommitTransaction()和AbortTransaction()事件中,编写了处理事务结果所需的代码。

11.1.5  事务事件

Transaction类提供了一个公共事件TransactionCompleted,该事件定义如下:

public delegate void TransactionCompletedEventHandler(object sender,

  TransactionEventArgs e);

在事务完成之后(事务得到提交或者放弃)触发TransactionCompleted事件。该事件是TransactionCompletedEventHandler委托类型,包括两个参数:sender表示刚刚完成的事务,e表示TransactionEventArgs类型,它提供了对同一事务的访问:

public class TransactionEventArgs: EventArgs

{

  public TransactionEventArgs();

  public Transaction Transaction{get;}

}

当事务完成时,可以通过订阅TransactionCompleted事件在事务完成时得到通知,如示例11-7所示。

示例11-7:处理TransactionCompleted事件

<%@ 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

        ["AdventureWorks"].ConnectionString;

      using (TransactionScope scope = new TransactionScope())

      {

        using (SqlConnection connection = new SqlConnection(connectionString))

        {

          Transaction trans = Transaction.Current;

          trans.TransactionCompleted += OnCompleted;

          categoryID = InsertCategory(connection);

        }

        //提交事务

        scope.Complete();

      }

      lblResult.Text =

        "Category is written successfully*****Category ID= " +

        categoryID.ToString();

    }

    catch (Exception ex)

    {

      lblResult.Text = "Exception is : " + ex.Message;

    }

  }

  void OnCompleted(object sender, TransactionEventArgs e)

  {

    Transaction transaction = e.Transaction;

    switch (transaction.TransactionInformation.Status)

    {

      case TransactionStatus.Aborted:

        {

          Response.Write("Transaction Aborted!");

          break;

        }

      case TransactionStatus.Committed:

        {

          Response.Write("Transaction Committed!");

          break;

        }

    }

  }

  int InsertCategory(SqlConnection connection)

  {

    //与前面的示例相同

  }

</script>

<html xmlns="http://www.w3.org/1999/xhtml">

<head runat="server">

  <title>Handling Transaction Events</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" />&nbsp;

      <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>

虽然开发人员知道何时启动LTM事务(当构造范围时),但是代码可能还想了解何时将LTM事务升级为分布式OleTx事务。静态类TransactionManager提供了TransactionStarted事件,定义如下:

public delegate void TransactionStartedEventHandler(object sender,

  TransactionEventArgs e);

当启动分布式事务时触发DistributedTransactionStarted事件。如下代码所示,开发人员能够订阅分布式事务的启动和完成事件:

public void DoWork()

{

TransactionManager.DistributedTransactionStarted += OnDistributedStarted;

using(TransactionScope scope = new TransactionScope())

{

    Transaction transaction = Transaction.Current;

    transaction.TransactionCompleted += OnCompleted;

    /*执行事务性工作*/

    scope.Complete();

}

}

void OnDistributedStarted(object sender,TransactionEventArgs e)

{...}

void OnCompleted(object sender,TransactionEventArgs e)

{...}

重要的是保证分布式事务的启动事件处理程序中所做工作的持续时间,因为知道所有订阅者都得到了通知,分布式事务才会启动。

11.1.6  System.Transactions和System.EnterpriseServices之间的交互

System.Transactions为.NET企业级服务提供了继承支持,这意味着服务组件能够获得新事务管理器和事务提升的优点,而无需做任何特殊的事情。有趣的问题是,当事务性服务组件创建TransactionScope对象,或者当事务性范围创建服务组件时发生了什么呢?由于企业级服务事务编程模型与对象声明周期和状态管理是耦合的,所以将其与不是基于对象的事务范围组合可能会导致某些复杂的影响。

System.Transactions在自身与企业服务之间定义了三层互操作性级别:None、Automatic和Full。EnterpriseServicesInteropOption枚举定义如下:

public enum EnterpriseServicesInteropOption

{

  Automatic,

  Full,

  None

}

TransactionScope类具有能接受EnterpriseServicesInteropOption的构造函数,例如:

public TransactionScope(TransactionScopeOption scopeOption,

  TransactionOptions transactionOptions,

  EnterpriseServicesInteropOption interopOption);

正如名称暗示的那样,EnterpriseServicesInteropOption.None意味着在企业级服务上下文与事务范围之间不进行互操作。这样的事务范围将完全忽略事务的上下文或者所创建的客户端,同时利用自身的事务。该事务与企业级服务管理的事务不同。结果,当企业级服务事务提交时,可能放弃事务范围的事务。以下代码说明如何在服务组件范围内使用TransactionScope类:

Transaction]

public class MyComponent : ServicedComponent

{

  [AutoComplete]

  public void DoWork()

  {

    TransactionOptions options = new TransactionOptions();

    options.IsolationLevel = IsolationLevel.Serializable;

    options.Timeout = TransactionManager.DefaultTimeout;

    using(TransactionScope scope = new TransactionScope(

      TransactionScopeOption.Required, options,

      EnterpriseServicesInteropOption.None)

    {

      //不调用scope.Complete()方法,但是COM+事务还是会提交

    }

  }

}

EnterpriseServicesInteropOption.None消除了来自企业级服务事务和事务混合的影响,因此它是一种安全的选择。TransactionScope默认使用这一选项,同时该TransactionScope的构造函数不可接受EnterpriseServicesInteropOption值。

如果不需要将企业级服务事务与System.Transactions事务组合起来,则需要使用EnterpriseServicesInteropOption.Automatic或者EnterpriseServicesInteropOption. Full。这两个值都依赖于服务域,因此需要运行在Windows XP SP2或者Windows 2003 Server中。EnterpriseServicesInteropOption.Full试图实现与服务组件尽可能的一样的行为。如果TransactionScope需要一个事务(加入现有事务,或者创建一个新事务),EnterpriseServicesInteropOption.Full将创建一个新的企业级服务事务的上下文,接着将其添加到任何现有的企业级服务事务中(或者创建新的企业级服务事务)。同时,所使用的System.Transactions事务与企业级服务上下文使用的将是同一个事务。如果TransactionScope对象不需要事务,则范围将被置于默认企业级服务上下文中。

11.1.7  何时使用事务

虽然.NET 2.0对事务提供了很好的支持,但是没有必要总是使用事务。一个更准确的规则是,在能够使用事务的时候都应该使用事务,但是不要使用过度。每次使用事务,都会占用一定的开销。另外,事务可能会锁定一些表的行。所以,不必要的事务会导致性能损失。这里有一个规则,只有当操作需要的时候才使用事务。例如,如果只是从数据库中查询一些记录,或者执行单个查询,在大部分时候都不需要显式的事务,因为声明都已经封装在隐式的事务中。但是,正如前文提到,在多声明更新时非常重要,因为事务能够实际提升操作速度。同样,如果需要在节省数毫秒时间和危害数据完整性之间做出一个选择的话,那么正确的答案就是保持数据清洁,不要担心那数毫秒的时间消耗。

当使用包括MSDTC的分布式事务时,影响非常明显。因为真正的分布式事务(在不同应用程序域中包括多个资源管理器,或者持久资源管理器)包括使用IsolationLevel.Serializable运行底层事务,分布式事务通常消耗非常高,而且容易出现问题。

事实上,在数据库或者ADO.NET环境下,只有在使用非数据库实体时,才使用分布式事务,像事务中自定义登记资源管理器,或者使用SQL在数据库中不能封装多数据库事务的情况。例如,在很多情况下,能够使用连接表或者连接服务器的方法。除非使用非数据库资源管理器,否则将不能在数据库中封装分布式事务,然后在事务结束时应用完整性检查。所以修复提交事务的错误可以保护事务的完整性。关键的事情是注意事务范围的最小限制。

事务和性能

在头脑中始终保持一个概念,就是用于修改多个不同表数据的冗长事务会严重妨碍系统中的所有其他用户。这很可能导致一些性能问题。当实现一个事务时,遵循下面的实践经验能够达到可接受的结果:

  ●    尽可能短的保持事务。

  ●    避免使用在事务中的SELECT返回数据,除非语句依赖于返回数据。

  ●    如果使用SELECT语句,只选择需要的行,因此不要锁定过多的资源同时保持尽可能高的性能。在架构语序的情况下,从事务中移出所有SELECT语句。

  ●    尽量将事务全部写在T-SQL或者API中。混合和匹配将导致混乱。同样,尽量在客户端使用API封装事务,而不是T-SQL。举例而言,当需要使用T-SQL完全封装事务时,如果那正是需要的,就可以完全接受。开发人员应该避免使用SqlTransaction作为事务的开始,然后使用存储过程回滚或者提交,或者其他类似方式。

  ●    避免事务与多重独立的批处理工作结合。将这些批处理放置在单独的事务中。

  ●    尽可能避免大量更新。当然这并不意味着为了避免大量更新而应该放弃事务的优越性。但是确保避免不必要的增加事务的大小非常重要,因为这么做会锁定更多资源。

必须注意的一点就是事务的默认行为。在默认情况下,如果没有显式地提交事务,则事务会回滚。虽然默认行为允许事务的回滚,但是显式回滚方法总是一个良好的编程习惯。这不仅仅只是释放锁定数据,也使得代码更容易读并且更少错误。

 

 

11.2  小结

本章介绍了ADO.NET的事务功能,还有企业级服务。除了编程处理事务外,还能应用[Transaction]属性指定事务需求来操作事务。事务性选项Required、RequiresNew、Supported、NotSupported和Disabled影响企业服务的监听代码,使得创建新事务,使用已存在的事务,没有事务都可以完全使用它。Windows Server 2003还能够提供名为无组件服务的新功能,使得无须从ServicedComponent类继承,便可使用System.EnterpriseServices的事务功能。

.NET Framework 2.0的事务过程是引人注目的改进。Framework增加了一个新的轻量级事务管理系统,并且公布了一个用于参与这类事务的简单架构。事务仅仅在执行本身需要的时候工作时才会使用资源。这意味着可以在启动时对每个事务使用DTC,当需要分布式事务管理服务时,事务变得非常灵活,只需要在DTC中登记它们即可。最后,必须牢记使用事务的方式,过度使用事务会导致性能受到消极影响。

本书至此,读者已经了解了很多ASP.NET 2.0功能,包括数据控件、事务等。下一章通过学习一个实例,讲解如何在真实的Web站点中使用这些功能。

 

摘自:http://book.csdn.net/bookfiles/518/10051817656.shtml