TransactionScope和分布式事务

时间:2021-02-20 11:11:19

分布式事务听起来很不错,其实不然。它只是尽可能的降低数据不一致的可能性,并不能完全避免。从我的应用中来看,总数约5千万的操作,错了十几个。当然,这个错误率完全可以忍受了。不能忍受的是当你的DBcluster(集群)当中,msdtc也会被作为一项资源出现,cluster的某些问题会诡异的导致msdtc不可用,问题排查起来是非常郁闷的。大家都知道,作为大型系统,不太可能不用cluster,所以msdtc的问题会显得很突出,伊给我的感觉实在是脆弱……

 

事务这个东东用来保证非常critical的数据的一致性,是否使用事务,要看你的业务需求.但是.NET 2.0在分布式事务支持上面不够彪悍,这使得某些情况下,你要牺牲一些东西,如:某些可以并行执行的部分,但是不能并行执行。System.Transactions中偶还没发现支持并行执行事务的Transactioin对象,所以如果想让代码比较“傻瓜”,就不得不串行执行所有的操作步骤,当然无法获得并行执行可并行步骤的快速响应。当然,系统的吞吐率可能因为串行提高了一点点,看你的需求了。

 

如果你不得不用分布式事务,那也得琢磨琢磨:

1.         这步操作一定得在事务当中吗?这步操作如果没完成或者失败了,值得回滚整个事务吗?难道没有优雅的补偿措施或者容错措施?

2.         分布式事务涉及到的点,必须的这么多?必须得实时的操作这一大串?不能通过通知类操作去精简掉某些点?

3.         在发起分布式事务之后,你是不是做了事务无关的操作,尽管这些操作跟事务无关?(如,读取数据、计算、等用户返回消息、等其他模块的调用返回等等)要知道事务应该尽快结束。

4.         你没有把一些读操作也算在事务里面了吧?这是很容易犯的错误,你在事务中Enlist了一个select 操作。

5.        你的操作,某些步骤可以等全部操作完成之后再执行.这类操作具有明显的通知类特点。通知类操作是说,我给你一个通知,并且我保证通知到了你;你必须吃下这个通知,并且保证处理成功,但是你不必我一通知你你就处理。这样的操作很明显可以用另外一个任务去搞。

and so on….
有经验的安达补充……

 

好吧,到这里你不得不使用传说中的“分布式事务”,那么,偶建议你首先在应用程序服务器安装如下几个补丁:

1.         kb916002.http://support.microsoft.com/kb/916002/en-us),它解决了“New request is not allowed to start because it should come with valid transaction descriptor.”的bug,不安装它,你的应用会出现大量这种错误。

2.      kb929246.http://support.microsoft.com/kb/929246/en-us),kb网站上说,它解决了“
Consider the following scenario. You connect two Microsoft SQL Server databases in a transaction scope. The transaction scope is defined by using the System.Transactions.TransactionScope class in the Microsoft .NET Framework 2.0. In this scenario, you intermittently receive the following error message:

System.InvalidOperationException: SqlConnection does not support parallel transactions

This problem only occurs if you enable connection pooling for the connection.

”。实际上你看到更多的是当你的TransactionAbort时(如超时),SqlConnection并没有被释放。直接后果是一段时间之后,你的SqlConnection连接池中没有可用连接,你无法拿到SqlConnection。当初报给GTech的时候感觉这种bug真是相当的愚蠢。是的,愚蠢!

3.         kb936983http://support.microsoft.com/kb/936983/en-us),kb网站上又说,丫解决了“
In a Microsoft .NET Framework 2.0-based application, you try to use the System.Transactions.CommittableTransaction.Commit method to commit a transaction. However, the call to the System.Transactions.CommittableTransaction.Commit method may always be blocked.

”。实际上你看到的情况是,你的程序运行了很长时间,但是你发现性能计数器.Net Provider for SqlServer\NumberOfStasisConnections开始有了示数,并且逐渐增加。于是你查看了代码,发现根本就不存在连接泄漏。接着你又开始查数据库,你发现确实有在前一天就Enlist到分布式事务中的连接在等待命令。然后,计数器示数.Net Provider for SqlServer\NumberOfStasisConnections连续增加,你意识到进程中的僵尸连接越来越多,为了避免连接池被吃光,你不得不偷偷的重启了服务,你甚至不得不为此写了一个脚本来定时重启你的服务。最后,你开始骂娘。相对2,这个bug稍微nb了一些。

 

我们有理由怀疑MS并没有仔细认真的测试System.TransactionsSystem.Data.SqlClient中的东西。

 

TransactionScope在文档中宣称只在“必要”情况下才提升事务级别,但是事实上不是这样。在TransactionScope内,只要你用不同的SqlConnection对象操作DB一次以上(不管你的目标是不是同一个实例、同一个库),都会提升事务级别到分布式事务。很猥琐吧?当然,从SqlClient目前实现上可以理解这个事情:

我们知道,在事务提交或回滚前,事务拿着的连接是不被释放的,不管你在代码中有没有调用CloseDispose(这里要稍微提一下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了。

==============

以上纯粹是个人经验,错误浅薄之处,还请各位安达指正.

谢谢~