TransactionScope:避免分布式事务

21
我有一个包含子对象集合(List)的父对象(DAL的一部分)。当我将对象保存回数据库时,我会输入/更新父对象,然后循环遍历每个子对象。为了可维护性,我将所有与子对象相关的代码放在一个单独的私有方法中。我本来想使用标准的 ADO 事务,但在我的尝试中,我偶然发现了 TransactionScope 对象,我相信它将使我能够在一个事务中包装父方法中的所有 DB 交互(以及子方法中的所有交互)。到目前为止还好吧...?

那么下一个问题是如何在这个 TransactionScope 中创建和使用连接。我听说即使是对同一个数据库使用多个连接也会迫使 TransactionScope 认为这是一个分布式事务(涉及一些昂贵的 DTC 工作)。这样吗?还是像我在其他地方看到的那样,使用相同的连接字符串(这将有助于连接池)就可以了呢?更实际的问题是...

  1. 在父级和子级中创建单独的连接(尽管具有相同的连接字符串)
  2. 在父级中创建一个连接并将其作为参数传递(对我来说似乎很笨拙)
  3. 还有别的方法吗...?

更新:

虽然看起来我可以使用我平常使用的.NET3.5+和SQL Server 2008+,但该项目的另一部分将使用Oracle(10g),因此我最好练习一种可以在项目中始终使用的技术。

所以我只需将连接传递给子方法即可。


选项1代码示例:

using (TransactionScope ts = new TransactionScope())
            {
                using (SqlConnection conn = new SqlConnection(connString))
                {
                    using (SqlCommand cmd = new SqlCommand())
                    {
                        cmd.Connection = conn;
                        cmd.Connection.Open();
                        cmd.CommandType = CommandType.StoredProcedure;

                        try
                        {
                            //create & add parameters to command

                            //save parent object to DB
                            cmd.ExecuteNonQuery();

                            if ((int)cmd.Parameters["@Result"].Value != 0)
                            {
                                //not ok
                                //rollback transaction
                                ts.Dispose();
                                return false;
                            }
                            else //enquiry saved OK
                            {
                                if (update)
                                {
                                    enquiryID = (int)cmd.Parameters["@EnquiryID"].Value;
                                }

                                //Save Vehicles (child objects)
                                if (SaveVehiclesToEPE())
                                {
                                    ts.Complete();
                                    return true;
                                }
                                else
                                {
                                    ts.Dispose();
                                    return false;
                                }
                            }
                        }
                        catch (Exception ex)
                        {
                            //log error
                            ts.Dispose();
                            throw;
                        }
                    }
                }
            }

3
请看TransactionScope automatically escalating to MSDTC on some machines?。有几个很好的答案,但我链接的那个最为简洁明了(也与你的问题相关)。总之,如果你正在使用.NET 2.0和SQL Server 2005,即使使用两个具有相同连接字符串的连接,你也会升级事务。但这不是.NET 3.5和SQL Server 2008的问题。 - Jeff Sternal
我通常使用.NET 3.5/4和SQL 2008,但偶尔可能会使用SQL2005/2000,所以还是值得记住的。谢谢。 - CJM
有人能给我一些关于分布式事务的知识吗?请用例子来解释。 - Thomas
3个回答

26

许多数据库 ADO 提供程序(例如 Oracle ODP.NET)确实会在使用 TransactionScope 跨多个连接进行事务时开始分布式事务,即使它们共享相同的连接字符串。

一些提供程序(如 .NET 3.5+ 中的 SQL2008)会识别当在引用相同连接字符串的事务范围内创建新连接时,并且不会导致 DTC 工作。但是,连接字符串中的任何变化(例如调整参数)都可能会阻止此类情况发生,行为将恢复为使用分布式事务。

不幸的是,确保事务可以一起工作而不创建分布式事务的唯一可靠方法是将连接对象(或 IDbTransaction)传递给需要“继续”在同一事务上执行的方法。

有时将连接升级为执行工作的类的成员有所帮助,但这可能会创建尴尬的情况,并且会复杂化控制连接对象的生命周期和处理(因为通常无法使用 using 语句)。


我曾经认为Connection必须在TransactionScope内创建才能被覆盖。我猜只需通过传递Connection比找出其他解决方法更简单、更整洁? - CJM
你知道这在Oracle 12c中是否有所改变吗? 我在这里看到它标记为“在ODAC 12c或更高版本中可用。”:https://apex.oracle.com/pls/apex/f?p=18357:39:1473540763666::NO::P39_ID:27121 - pauloya

3

根据经验,我已经确定(对于SQL Server提供程序)如果进程可以利用连接池共享连接(和事务)在父进程和子进程之间,DTC不一定会介入。

然而,这是一个很大的“如果”,正如您的示例所述,父进程创建的连接无法被子进程共享(您在调用子进程之前没有关闭/释放连接)。这将导致跨越两个实际连接的事务,从而导致事务升级为分布式事务。

看起来很容易重构您的代码以避免这种情况:只需在调用子进程之前关闭父进程创建的连接即可。


你的意思是说,如果我通过连接对象传递,我需要在父级中关闭并在子级中重新打开吗? - CJM
不,我原以为你试图利用TransactionScope的便利性,而不会导致事务升级为分布式事务。听起来在针对Oracle数据库时这是不可能的,但是在针对SQL Server数据库时是可能的……只要连接可以池化并且一次只有一个打开的连接。 - Daniel Pratt
知道这点很好。无论如何,在使用下一个连接之前关闭DbConnection是一个好的实践,如果这可以可靠地避免分布式事务(假设连接字符串相同),那就更好了! - Timo

1
在您的示例中,TransactionScope仍然处于方法的上下文中,您可以在其中创建具有多个命令的SqlTransaction。如果要将事务移出方法,例如调用该方法的调用者,或者访问多个数据库,则使用TransactionScope。
更新:算了,我刚刚发现子调用。在这种情况下,您可以将连接对象传递给子类。此外,您不需要手动处理TransactionScope - 使用块就像try-finally块一样,并且即使在异常情况下也会执行处理。
更新2:更好的方法是将IDbTransaction传递给子类。连接可以从中检索。

是的,我明白不需要显式地进行处理,但当我将TransactionScope改装到我的代码中时,显然我忘记了,并继续将旧的trm.Rollback语句转换为ts.Dispose而没有思考。很好发现! - CJM
更新2 - 您可以将IDbConnection传递给子类。如果使用IbConnection.BeginTransaction(),则使用IDbConnection.CreateCommand()创建的所有命令都将自动关联事务。这更好,因为它是一个不需要在继承中传递的参数,并且减少了耦合,因为子类不需要考虑它们是否在事务中执行。 - Richard Dingwall

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接