• 事务与系统架构设计


    做项目或系统设计时,依需求的不同,适必有不同的解决方案,有的以性能为主,有的以可扩展性为主,有的为了日后易于维护而做大量的组件化。本帖依此提供三种不同特性的「事务」ASP.NET 示例下载,包括:用一个数据库 Connection 即可高性能跨数据库写入、透过组件的函数调用即可参与事务、异步 (Asynchronous) 执行事务。

    三个 ASP.NET 示例,其「事務」特性分別為:

    (1) 兼顾性能与功能 - 利用 SqlConnection 类的 ChangeDatabase 方法,在单一个 Connection 中,跨越本机的两个数据库做 LTE (轻量级) 事务。
    (2) 追求良好的架构、组件化及可维护性 - 利用 TransactionScope 类 + MS DTC,直接经由各组件之间的函数调用,将其纳入同一个事务,亦可升级为 OleTx 分布式事务。
    (3) 重视回应速度与用户体验 - 利用 CommittableTransaction + AsyncCallback 类,进行明确式的「异步 (Asynchronous)」事务。

    -------------------------------------------------
    本帖的示例下载点:
    https://files.cnblogs.com/WizardWu/100204.zip
    (执行第一個示例,需要 SQL Server 的 Northwind、AdventureWorksDW 数据库,不需要 DTC)
    (执行第二個示例,需要 SQL Server 的 Northwind 数据库,并事先设置好 Windows 上的 DTC 分布式事务处理协调器)
    (执行第三個示例,需要 SQL Server 的 Northwind 数据库,不需要 DTC)
    ---------------------------------------------------

    (一) 示例一:兼顾性能与功能

      

    有时我们只是临时需要在某一台机器上的 SQL Server,跨越其中的两个数据库做事务处理,或是其他一些简易的本机事务处理,此时只要透过一些 ADO.NET 的小技巧,利用同一个 Connection 对象,和最传统的 SqlTransaction 即可办到。如下方代码,透过 SqlConnection 的 ChangeDatabase 方法,即可在 Northwind、AdventureWorksDW 两个数据库之间切换,无须大费周章地升级为分布式事务,或浪费资源创建两次数据库的 Connection。

    示例一
    protected void Button1_Click(object sender, EventArgs e)
    {
    SqlConnection cn
    = new SqlConnection("server=localhost;database=Northwind;integrated security=true");
    SqlTransaction tx
    = null;
    try
    {
    cn.Open();
    tx
    = cn.BeginTransaction();
    SqlCommand cmd1
    = new SqlCommand("INSERT INTO Employees (LastName, FirstName) VALUES('Wu', 'Wizard')", cn);
    cmd1.Transaction
    = tx;
    cmd1.ExecuteNonQuery();


    cn.ChangeDatabase("AdventureWorksDW");


    SqlCommand cmd2
    = new SqlCommand("INSERT INTO DimGeography (City) VALUES ('Taipei')", cn);

    cmd2.Transaction
    = tx;

    cmd2.ExecuteNonQuery();
    tx.Commit();
    Response.Write(
    "跨越两个数据库的 LTE 本机事务成功 !");
    }
    catch (SqlException ex)
    {
    tx.Rollback();
    Response.Write(
    "发生错误: " + ex.Message);
    }
    finally
    {
    cn.Close();
    cn.Dispose();
    }
    }

     市面上有好几本专讲 ADO.NET 的中、英文书籍,内容都相当不错,只可惜这方面的议题较少受到重视。

    ----------------------------------------------------------------------------

    (二) 示例二:追求良好的架构、组件化及可维护性

    有些写 Java 或比较重视架构设计的工程师,常会将一些特定的功能或商业逻辑,各自封装在多个组件或类之中 (Java 中的 Bean 或 SessionBean)。微软方面,自从 .NET 2.0 问世、TransactonScope 类和新世代的事务管理机制出现后,以往用 COM+ 的写法才能达到的功能,现在用 TransactonScope 类竟然很轻松地就能达成,这让 OOA/OOD、面向对象和 Design Patterns 的爱好者,在 .NET 平台上有了很好的解套方式。亦即可让对象的行为,在架构设计上能够独立,但却能随时决定是否要参与某个事务,或动态地决定是否要从 Local 事务升级成分布式事务。

    例如下方的代码,为两个类 (或组件) 里各自的函数,他们可能是 ERP 中的「订单产生」组件,要调用「仓库对象」组件,去扣除一些库存量。透过「巢状 (nested);嵌套」的二或多个  TransactonScope 类,以及函数的直接调用,即可将对方纳入此一事务,并可自定义是否要纳入成为同一个事务,并且升级成分布式事务、启动 DTC,抑或拆分成两个事务、不启动 DTC。且不论是哪种选项,都能达到任一方抛出 Exception 时,双方都能自动 Rollback。

    Class1
    {
    private void func1()
    {
    using (TransactionScope scope = new TransactionScope())
    {
    Class2 c2
    = new Class2();
    c2.func2();
    //调用另一个组件的函数,直接将它纳入事务
                scope.Complete();
    }
    }
    }


    Class2

    {
    public void func2()
    {
    using (TransactionScope scope = new TransactionScope())
    {
    scope.Complete();
    }
    }
    }

    下图 1 为本帖下载示例 - 示例二的执行画面。如前述,我们用两个 Class 中函数调用的做法,但 Class 1、Class 2 的 TransactionScope,其 TransactionScopeOption 都设置为 Required (若已有现存的事务,则参与该个事务),表示双方要加入「同一个」事务中。因此 Class 1 所插入数据库的一条记录,Class 2 立即可 SELECT 得到它,因为他们是在「同一个」事务中。但代价是会启动 MS DTC、自动升级成 OleTx 分布式事务。虽然这两个 Class 是在同一台机器中,但因为在同一个事务中,开启了两条数据库的 Connection,因此仍会自动从本机的轻量级 LTM 事务管理员,升级成 OleTx 事务管理员 (依赖 RPC 远端程序调用),也因此会自动启用 MS DTC (若 DTC 已设置好)。

    但若您把 Class 2 的 TransactionScope,其 TransactionScopeOption 设置为 RequiresNew (不管是否有现存事务,都一律创建新的事务),您会发现 MS DTC 不会启动了,因为他们已被拆分成「二个事务」,也因此 Class 1 所插入数据库的一条记录,Class 2 已无法立即 SELECT 取得,因为他们不在「同一个」事务中。

    但不论是前述哪种做法,仍都能达到任一方引发 Exception 时,双方都能自动 Rollback。若您以前,曾经梦想透过 Web Service 彼此的调用,来达到事务的完整性,会发现情形如同前述的第二种,亦即被拆分成「二个事务」,虽然任一方抛出 Exception 时,双方都能自动 Rollback,但由于是拆分成二个事务,因此第一个 Web Service 所插入数据库的一条记录,第二个 Web Service 无法立即取得。而这点,就某些系统的设计需求上,虽然看似小瑕疵,却是不被允许的。可能有些人宁愿用第一种做法,包成「同一个」事务,宁可启动 MS DTC,牺牲一些性能,也要达成事务的高度完整性。


    图 1 示例二的执行画面

    示例二的 Class1 (组件一)
    using System;
    using System.Data;

    using System.Transactions;
    using System.Data.SqlClient;

    public class Class1
    {
    private string strConnString = System.Configuration.ConfigurationManager.ConnectionStrings["Conn_Northwind"].ToString();

    public Class1()
    {
    }

    public string func1()
    {
    SqlConnection conn
    = null;
    SqlCommand cmd
    = null;
    int intTheNewestID = 0;
    string strReturn = "";

    //Insert 后,立即 Select 出数据库最新插入的这一笔记录,其 id 值 (identity, 由数据库自动增号)
    string strSql = "INSERT INTO Employees (LastName, FirstName) VALUES('Wu', 'Wizard') ; SELECT @@identity; ";

    //Required 选项: 当前环境若无事务,则创建新事务,否则就加入当前环境的同一个事务
    using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Required))
    {
    try
    {
    conn
    = new SqlConnection(strConnString);
    conn.Open();
    if (conn.State == ConnectionState.Open)
    {
    cmd
    = new SqlCommand(strSql, conn);
    intTheNewestID
    = Convert.ToInt32(cmd.ExecuteScalar());

    //调用 Class 2 的函数,将其也加入同一个事务
    Class2 c2 = new Class2();
    strReturn
    = c2.func2(intTheNewestID);

    scope.Complete();
    }
    }
    catch (Exception ex)
    {
    throw new Exception("组件一 - 发生数据库访问错误: " + ex.ToString());
    }
    finally
    {
    if (cmd != null)
    cmd.Dispose();
    if (conn.State == ConnectionState.Open)
    {
    conn.Close();
    }
    conn.Dispose();
    }
    }

    return strReturn; //返回前台的网页中显示
    }
    }
    示例二的 Class2 (组件二)
    using System;
    using System.Data;

    using System.Transactions;
    using System.Data.SqlClient;

    public class Class2
    {
    private string strConnString = System.Configuration.ConfigurationManager.ConnectionStrings["Conn_Northwind"].ToString();

    public Class2()
    {
    }

    public string func2(int intTheNewestID)
    {
    SqlConnection conn
    = null;
    SqlCommand cmd1
    = null;
    SqlCommand cmd2
    = null;
    int intInserted = 0;
    string strReturn = "";
    string strSql1 = "INSERT INTO Employees (LastName, FirstName) VALUES('Lee', 'David')";
    string strSql2 = "SELECT LastName FROM Employees WHERE EmployeeID=" + intTheNewestID;

    //Required 选项: 当前环境若无事务,则创建新事务,否则就加入当前环境的同一个事务。在此例中,会启动 DTC,第二句 Select 会成功。
    //RequiresNew 选项: 总是创建新的事务,会造成 Class1、Class2 不会处于同一个事务里。在此例中,不会启动 DTC,第二句 Select 会失败。
    //Suppress 选项: 不加入此一事务。
    using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Required))
    {
    try
    {
    conn
    = new SqlConnection(strConnString);
    conn.Open();
    if (conn.State == ConnectionState.Open)
    {
    cmd1
    = new SqlCommand(strSql1, conn);
    intInserted
    = cmd1.ExecuteNonQuery();

    //取得组件一里,刚刚才插入的那一笔记录,以确认组件一、组件二确实是在同一个事务中执行,而不是拆分成两个事务
    cmd2 = new SqlCommand(strSql2, conn);
    strReturn
    = cmd2.ExecuteScalar().ToString();

    scope.Complete();
    }
    }
    catch (Exception ex)
    {
    throw new Exception("组件二 - 数据库访问发生错误: " + ex.ToString());
    }
    finally
    {
    if (cmd1 != null)
    cmd1.Dispose();
    if (cmd2 != null)
    cmd2.Dispose();
    if (conn.State == ConnectionState.Open)
    {
    conn.Close();
    }
    conn.Dispose();
    }
    }

    return strReturn; //返回组件一
    }
    }

    MSDN 上有一篇文章 [1],或一些 ADO.NET 书籍,有介绍此种 Nested TransactionScope,及其 TransactionScopeOption 的设置。如下图 2,最左侧为没有事务的代码,当其调用了 scope1 时 (Required),创建了全新的事务 Transaction A。接下来,当创建了第二个 scope2,或如本帖示例二调用了第二个组件时,由于也是 Reuqired,因此和本帖示例二的情况一模一样,双方会包在「同一个」事务 A 中,并可能会启动 MS DTC。

    当创建了第三个 scope3,或呼叫了第三个组件时,由于是 ReuqiresNew,因此会创建「另一个」事务 Transaction B。而当创建了第四个 scope4,或调用了第四个组件时,因设置为 Suppress (表示无论如何不加入事务),因此其会独立执行,不参与任何事务。此种 Supppress 设置,适用于调用第三方厂商或协力厂商的组件,或是单纯执行 SELECT 语句,不需要或不想加入事务时的情形。

    //Default TransactionScopeOption is "Required"
    using(TransactionScope scope1 = new TransactionScope())
    {
    using(TransactionScope scope2 = new TransactionScope(TransactionScopeOption.Required))
    {...}

    using(TransactionScope scope3 = new TransactionScope(TransactionScopeOption.RequiresNew))
    {...}

    using(TransactionScope scope4 = new TransactionScope(TransactionScopeOption.Suppress))
    {...}

    //...
    }

    图 2 不同 TransactionScopeOption 设置的执行结果


    在我先前写过的文章「网站性能优化 - 数据库及服务器架构篇」,里面的图 3 -「物理」上的分层,各种商业逻辑可能存在多台物理主机上,里面有提到,这些不同功能的组件或商业逻辑,可能在同一台 AP Server  上,也可能分布在不同的服务器上。因此要以哪种方式来调用,或同一台机器上的组件,是否有必要牺牲一些性能、启用 DTC 来运作,以达成特定需求的系统设计,应事先做好评估。

    ----------------------------------------------------------------------------

    (三) 示例三:重视回应速度与用户体验

    若事务访问了多个数据库,或因网络太慢,让事务时间拉太长,我们还可考虑用 CommittableTransaction 类,以「异步 (Asynchronous)」方式来处理事务。其原理为利用另一条背景线程,来等待事务处理的结果,让主程序 (客户端的浏览器) 能先进行其他的操作,避免让用户处于等待的情况。

    如下方示例三的部分代码,执行异步事务时,需提供一个 Callback 方法,在 Commit 时自动调用,亦即下方示例的 OnCommitted 方法。当执行到这个方法时,便会从 Thread Pool 里取得一条线程,进行异步的事务确认。

    示例三
    using System;

    using System.Data;
    using System.Transactions;
    using System.Data.SqlClient;

    public partial class _Default : System.Web.UI.Page
    {
    private string strConnString = System.Configuration.ConfigurationManager.ConnectionStrings["Conn_Northwind"].ToString();

    protected void Page_Load(object sender, EventArgs e)
    {
    }

    protected void Button1_Click(object sender, EventArgs e)
    {
    SqlConnection conn
    = null;
    SqlCommand cmd
    = null;

    string strSql = "INSERT INTO Employees (LastName, FirstName) VALUES('Wu', 'Wizard')";

    //用 CommittableTransaction 进行明确式事务
    using (CommittableTransaction tran = new CommittableTransaction())
    {
    try
    {
    conn
    = new SqlConnection(strConnString);
    conn.Open();
    conn.EnlistTransaction(tran);
    if (conn.State == ConnectionState.Open)
    {
    cmd
    = new SqlCommand(strSql, conn);
    cmd.ExecuteNonQuery();

    //指定 Callback 函数为 OnCommitted
    AsyncCallback ac = new AsyncCallback(OnCommitted);
    tran.BeginCommit(ac,
    null); //开始一个异步事务

    //tran.Commit(); //同步事务的写法
    }
    }
    catch (Exception ex)
    {
    tran.Rollback();
    Response.Write(
    "程序发生错误: " + ex.Message);
    }
    finally
    {
    if (cmd != null)
    cmd.Dispose();
    if (conn.State == ConnectionState.Open)
    {
    conn.Close();
    }
    conn.Dispose();
    }
    }
    }

    //执行到这个方法时,会从 Thread Pool 里取得一条线程,进行异步的事务
    private void OnCommitted(IAsyncResult ar) //传入一个 IAsyncResult 参数
    {
    CommittableTransaction Tx;
    Tx
    = (CommittableTransaction)ar;

    try
    {
    using ((Tx))
    {
    Tx.EndCommit(ar);
    //结束异步事务
    }

    Response.Write(
    "异步事务完成,已成功插入一条记录。");
    }
    catch (TransactionException ex)
    {
    Tx.Rollback();
    Response.Write(
    "异步事务失败,错误信息为:" + ex.Message);
    }
    finally
    {
    if (Tx != null)
    Tx.Dispose();
    }
    }

    }

    ----------------------------------------------------------------------------

    本帖第一、第三个示例,执行时并不会启动 MS DTC;而第二个示例,则要看 TransactionScopeOption 的设置情形,依本帖下载示例的缺省值,由于双方都为 Required,因此默认会启动 DTC;但若您将示例中 Class 2 里 func 2 改为 RequiresNew,则不会启动 DTC。因此实务上,一个系统该如何去设计,是否要为了彻底的组件化、易于日后维护和扩展,而牺牲一些事务处理上的性能 (写 Java/J2EE 的人好像常干这种事),应视系统和项目的需求,而非永远以一套固定的设计方式或代码写法,就想套用在所有的项目中。

    图 3 MS DTC 统计画面

    ----------------------------------------------------------------------------

    相关文章:

    [1] Introducing System.Transactions in the .NET Framework 2.0
    http://msdn.microsoft.com/en-us/library/ms973865.aspx

    [2] J2EE与.NET在Transaction Scope上的比较
    http://www.cnblogs.com/perhaps/archive/2005/08/17/216863.html

    [3] SQL Server 的 System.Transactions 集成 (ADO.NET)
    http://msdn.microsoft.com/zh-cn/library/ms172070.aspx

    [4] 谈谈分布式事务(Distributed Transaction)[共5篇] - Artech - 博客园
    http://www.cnblogs.com/artech/archive/2010/01/31/1660433.html

    [5] WCF系列_分布式事务
    http://www.cnblogs.com/chnking/archive/2010/01/10/1643362.html
    http://www.cnblogs.com/chnking/archive/2010/01/10/1643384.html

    [6] 网站性能优化 - 数据库及服务器架构篇
    http://www.cnblogs.com/WizardWu/archive/2009/09/22/1571499.html

    ----------------------------------------------------------------------------

  • 相关阅读:
    设计一种配置文件格式(草稿)
    linux shell控制语句
    CuteC 发布(2011519)
    国外兼职外包项目大全
    寻找第K大的数的方法总结
    IEEE PDF eXpress 使用
    找出一个不在文件中的整数 编程珠玑
    BM模式匹配算法实现(C语言)
    一种可做特殊用途的字符串匹配算法
    字符设备驱动程序
  • 原文地址:https://www.cnblogs.com/WizardWu/p/1663127.html
Copyright © 2020-2023  润新知