TransactionScope分布式事务和非分布式事务

时间:2021-06-07 23:25:52
分布式事务听起来很不错,其实不然。它只是尽可能的降低数据不一致的可能性,并不能完全避免。从我的应用中来看,总数约5千万的操作,错了十几个。当然,这个错误率完全可以忍受了。不能忍受的是当你的DB在cluster(集群)当中,msdtc也会被作为一项资源出现,cluster的某些问题会诡异的导致msdtc不可用,问题排查起来是非常郁闷的。大家都知道,作为大型系统,不太可能不用cluster,所以msdtc的问题会显得很突出,伊给我的感觉实在是脆弱……
 
事务这个东东用来保证非常critical的数据的一致性,是否使用事务,要看你的业务需求.但是.NET 2.0在分布式事务支持上面不够彪悍,这使得某些情况下,你要牺牲一些东西,如:某些可以并行执行的部分,但是不能并行执行。System.Transactions中偶还没发现支持并行执行事务的Transactioin对象,所以如果想让代码比较“傻瓜”,就不得不串行执行所有的操作步骤,当然无法获得并行执行可并行步骤的快速响应。当然,系统的吞吐率可能因为串行提高了一点点,看你的需求了。
 
如果你不得不用分布式事务,那也得琢磨琢磨:
1.         这步操作一定得在事务当中吗?这步操作如果没完成或者失败了,值得回滚整个事务吗?难道没有优雅的补偿措施或者容错措施?
2.    分布式事务涉及到的点,必须的这么多?必须得 实时的操作这一大串?不能通过通知类操作去精简掉某些点?
3.         在发起分布式事务之后,你是不是做了事务无关的操作,尽管这些操作跟事务无关?(如,读取数据、计算、等用户返回消息、等其他模块的调用返回等等)要知道事务应该尽快结束。
4.         你没有把一些读操作也算在事务里面了吧?这是很容易犯的错误,你在事务中Enlist了一个select 操作。
5.        你的操作,某些步骤可以等全部操作完成之后再执行.这类操作具有明显的通知类特点。通知类操作是说,我给你一个通知,并且我保证通知到了你;你必须吃下这个通知,并且保证处理成功,但是你不必我一通知你你就处理。这样的操作很明显可以用另外一个任务去搞。
TransactionScope在文档中宣称只在“必要”情况下才提升事务级别(多数据库时才使用分布式事务,如果是同一个数据库,最好使用SqlTransaction),但是事实上不是这样。在TransactionScope内,只要你用不同的SqlConnection对象操作DB一次以上(不管你的目标是不是同一个实例、同一个库),都会提升事务级别到分布式事务。很猥琐吧?当然,从SqlClient目前实现上可以理解这个事情:
我们知道,在事务提交或回滚前,事务拿着的连接是不被释放的,不管你在代码中有没有调用Close或Dispose(这里要稍微提一下Dispose模式。对这两个方法的调用没有本质上的不同)。 所以就算是用相同的连接字符串生成的俩SqlConnection,其内部引用的连接池中的internal连接也不是一个,也就是说,从DB的角度看,是两个不同的Session.不同的Session不能共享一个本地事务,因为连接不是同一个,只能通过分布式事务管理。
但是微软不能优化它吗?能。连接字符串完全可以解析成一堆field,就像SqlConnectionStringBuilder那样,然后通过比较前面一个internal的连接中信息来判断是重用前一个连接还是从连接池里面拿一个新的。当然还有一个坎, 有任何一点不同的连接字符串都会生成不同的连接池,就算是只多一个空格也是这样,连接池可能也得改造改造。
但是微软没有做这种改造,那我们就得自己做了——执行相同实例相同库上操作都在一个SqlConnection上执行好了。
        
值得一提的是,DbConnection对象有一个EnlistTransaction方法,它给了我们手工分布式事务的机会。现在我们设计东西,大多数情况下是采用从上到下的设计,最后才会关心persistent方式。这个时候DbConnection.EnlistTransaction就显得特别的有用。相比之下,TransactionScope过于死板。而且,手工控制Enlist给了我们并行执行一堆操作,最后大家一起提交或回滚的可能(只要你的需求能忍受稍高的错误率。分布式事务的两阶段提交方式理论上注定这个所谓的“稍高”不会比直接使用分布式事务高多少。)
        
最后稍微提提MSDTC的配置和DTCPing工具。
MSDTC配置主要是“安全配置”,并且要在应用程序服务器(应用程序服务器也是分布式事务的一个节点)、所有相关的DB服务器上进行配置。怎么配置google一把,到处都是。
配置完之后,应该把DTCPing拷贝到所有这些服务器上,并且进行 双向的连通性确认。双向是说A->B得通,B->A也得通。注意DTCPing的用法,上面写的明明白白,就不多说了。
如果某台机器不能解析别的机器名,修改一把hosts文件就OK了。
 
2) SqlTransaction 对象
用Connection对象的BeginTransaction方法生成 SqlTransaction对象
指定SqlCommand对象的SqlTransaction对象
SqlTransaction对象的Commit() 方法和Rollback方法
 
在提交或回滚 SqlTransaction 时,应始终使用 Try/Catch 进行异常处理。如果连接终止或事务已在服务器上回滚,则 Commit 和 Rollback 都会生成 InvalidOperationException。

成员函数Save(string rollbackstring) 在事务中创建保存点(它可用于回滚事务的一部分),并指定保存点名称。如 SqlTransaction.save("NoUpdate") // 创建回滚点。 SqlTransaction.Rollback("NoUpdate")//回滚到回滚点

 
public void RunSqlTransaction(string myConnString)
     {
    SqlConnection myConnection = new SqlConnection(myConnString);
    myConnection.Open();

    SqlCommand myCommand = new SqlCommand();
    SqlTransaction myTrans;

    // Start a local transaction
    myTrans = myConnection. BeginTransaction(IsolationLevel.ReadCommitted,"SampleTransaction");
    // Must assign both transaction object and connection
    // to Command object for a pending local transaction
    myCommand.Connection = myConnection;
    myCommand.Transaction = myTrans;

    try
    {
      myCommand.CommandText = "Insert into Region (RegionID, RegionDescription)
VALUES (100, 'Description')";
      myCommand.ExecuteNonQuery();
      myCommand.CommandText = "Insert into Region (RegionID, RegionDescription)
VALUES (101, 'Description')";
      myCommand.ExecuteNonQuery();
      myTrans.Commit();
      Console.WriteLine("Both records are written to database.");
    }
    catch(Exception e)
    {
      myTrans.Rollback("SampleTransaction");
      Console.WriteLine(e.ToString());
      Console.WriteLine("Neither record was written to database.");
    }
    finally
    {
      myConnection.Close();
    }
}
一.数据库事务的ACID属性

   事务处理可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的资源。通过将一组相关操作组合为一个要么全部成功要么全部失败的单元,可以简化错误恢复并使应用程序更加可靠。一个逻辑工作单元要成为事务,必须满足所谓的ACID(原子性、一致性、隔离性和持久性)属性:

       原子性

       事务必须是原子工作单元;对于其数据修改,要么全都执行,要么全都不执行。通常,与某个事务关联的操作具有共同的目标,并且是相互依赖的。如果系统只执行这些操作的一个子集,则可能会破坏事务的总体目标。原子性消除了系统处理操作子集的可能性。

  一致性

        事务在完成时,必须使所有的数据都保持一致状态。在相关数据库中,所有规则都必须应用于事务的修改,以保持所有数据的完整性。事务结束时,所有的内部数据结构(如 B 树索引或双向链表)都必须是正确的。某些维护一致性的责任由应用程序开发人员承担,他们必须确保应用程序已强制所有已知的完整性约束。例如,当开发用于转帐的应用程序时,应避免在转帐过程中任意移动小数点。

  隔离性

        由并发事务所作的修改必须与任何其它并发事务所作的修改隔离。事务查看数据时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看中间状态的数据。这称为可串行性,因为它能够重新装载起始数据,并且重播一系列事务,以使数据结束时的状态与原始事务执行的状态相同。当事务可序列化时将获得最高的隔离级别。在此级别上,从一组可并行执行的事务获得的结果与通过连续运行每个事务所获得的结果相同。由于高度隔离会限制可并行执行的事务数,所以一些应用程序降低隔离级别以换取更大的吞吐量。

  持久性

      事务完成之后,它对于系统的影响是永久性的。该修改即使出现致命的系统故障也将一直保持。
DBMS的责任和我们的任务

       企业级的数据库管理系统(DBMS)都有责任提供一种保证事务的物理完整性的机制。就常用的SQL Server2000系统而言,它具备锁定设备隔离事务、记录设备保证事务持久性等机制。因此,我们不必关心数据库事务的物理完整性,而应该关注在什么情况下使用数据库事务、事务对性能的影响,如何使用事务等等。

 
   隔离级别的概念

        企业级的数据库每一秒钟都可能应付成千上万的并发访问,因而带来了并发控制的问题。由数据库理论可知,由于并发访问,在不可预料的时刻可能引发如下几个可以预料的问题:

   脏读:包含未提交数据的读取。例如,事务1 更改了某行。事务2 在事务1 提交更改之前读取已更改的行。如果事务1 回滚更改,则事务2 便读取了逻辑上从未存在过的行。

   不可重复读取:当某个事务不止一次读取同一行,并且一个单独的事务在两次(或多次)读取之间修改该行时,因为在同一个事务内的多次读取之间修改了该行,所以每次读取都生成不同值,从而引发不一致问题。

   幻象:通过一个任务,在以前由另一个尚未提交其事务的任务读取的行的范围中插入新行或删除现有行。带有未提交事务的任务由于该范围中行数的更改而无法重复其原始读取。

       如你所想,这些情况发生的根本原因都是因为在并发访问的时候,没有一个机制避免交叉存取所造成的。而 隔离级别的设置,正是为了避免这些情况的发生。事务准备接受不一致数据的级别称为 隔离级别隔离级别是一个事务必须与其它事务进行隔离的程度。较低的 隔离级别可以增加并发,但代价是降低数据的正确性。相反,较高的 隔离级别可以确保数据的正确性,但可能对并发产生负面影响。

       根据 隔离级别的不同,DBMS为并行访问提供不同的互斥保证。在SQL Server数据库中,提供四种 隔离级别:未提交读、提交读、可重复读、可串行读。这四种 隔离级别可以不同程度地保证并发的数据完整性:

 

隔离级别 脏 读 不可重复读取 幻 像
未提交读
提交读
可重复读
可串行读

 

       可以看出,“可串行读”提供了*别的隔离,这时并发事务的执行结果将与串行执行的完全一致。如前所述,*别的隔离也就意味着最低程度的并发,因此,在此隔离级别下,数据库的服务效率事实上是比较低的。尽管可串行性对于事务确保数据库中的数据在所有时间内的正确性相当重要,然而许多事务并不总是要求完全的隔离。例如,多个作者工作于同一本书的不同章节。新章节可以在任意时候提交到项目中。但是,对于已经编辑过的章节,没有编辑人员的批准,作者不能对此章节进行任何更改。这样,尽管有未编辑的新章节,但编辑人员仍可以确保在任意时间该书籍项目的正确性。编辑人员可以查看以前编辑的章节以及最近提交的章节。这样,其它的几种隔离级别也有其存在的意义。

     在.net框架中,事务的隔离级别是由枚举System.Data.IsolationLevel所定义的:  
 

 

[Flags]
[Serializable]
public enum IsolationLevel

 

     其成员及相应的含义如下:

 

成 员 含 义
Chaos 无法改写隔离级别更高的事务中的挂起的更改。
ReadCommitted 在正在读取数据时保持共享锁,以避免脏读,但是在事务结束之前可以更改数据,从而导致不可重复的读取或幻像数据。
ReadUncommitted 可以进行脏读,意思是说,不发布共享锁,也不接受独占锁。
RepeatableRead 在查询中使用的所有数据上放置锁,以防止其他用户更新这些数据。防止不可重复的读取,但是仍可以有幻像行。
Serializable 在DataSet上放置范围锁,以防止在事务完成之前由其他用户更新行或向数据集中插入行。
Unspecified 正在使用与指定隔离级别不同的隔离级别,但是无法确定该级别。
 

 

     显而意见,数据库的四个隔离级别在这里都有映射。

      默认的情况下,SQL Server使用ReadCommitted(提交读)隔离级别

    关于隔离级别的最后一点就是如果你在事务执行的过程中改变了隔离级别,那么后面的命名都在最新的隔离级别下执行——隔离级别的改变是立即生效的。有了这一点,你可以在你的事务中更灵活地使用隔离级别从而达到更高的效率和并发安全性。