TransactionScope:避免分布式事务

Posted

技术标签:

【中文标题】TransactionScope:避免分布式事务【英文标题】:TransactionScope: Avoiding Distributed Transactions 【发布时间】:2011-03-12 09:15:31 【问题描述】:

我有一个父对象(DAL 的一部分),其中包含子对象的集合 (List<t>)。

当我将对象保存回数据库时,我输入/更新父对象,然后遍历每个子对象。为了可维护性,我将孩子的所有代码都放在了一个单独的私有方法中。

我打算使用标准的 ADO 事务,但在旅途中,我偶然发现了 TransactionScope 对象,我相信这将使我能够将所有数据库交互(以及子方法中的所有交互)包装在一笔交易。

到目前为止一切顺利..?

所以下一个问题是如何在这个 TransactionScope 内创建和使用连接。我听说使用多个连接,即使它们连接到同一个数据库也会迫使 TransactionScope 认为它是一个分布式事务(涉及一些昂贵的 DTC 工作)。

是这样吗?或者,正如我似乎在其他地方读到的那样,使用相同的连接字符串(这将有助于连接池)的情况会很好吗?

更实际地说,我是不是……

    在父级和子级中创建单独的连接(尽管使用相同的连接字符串) 在父级中创建一个连接并将其作为参数传递(对我来说似乎很笨拙) 做点别的...?

更新:

虽然看起来我可以使用我常用的 .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;
                        
                    
                
            

【问题讨论】:

见TransactionScope automatically escalating to MSDTC on some machines?。有几个很好的答案,但我链接的一个是最简洁的(并且与您的问题相关)。结果是,如果您使用的是 .NET 2.0 和 SQL Server 2005,即使使用具有相同连接字符串的两个连接,您也会升级。这不是 .NET 3.5 和 SQL Server 2008 的问题。 am 通常使用 .NET 3.5/4 和 SQL 2008,但偶尔我可能会使用 SQL2005/2000,所以无论如何都值得记住。谢谢 谁能给我一些关于什么是分布式事务的知识。举例说明。 【参考方案1】:

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

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

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

有时,将连接提升到您正在执行工作的类的成员会有所帮助,但这可能会造成尴尬的情况 - 并使控制连接对象的生命周期和处置变得复杂(因为它通常会排除使用using 声明)。

【讨论】:

我的印象是必须在 TransactionScope 中创建 Connection 才能覆盖它。我猜简单地通过 Connection 比找出其他解决方法更简单、更整洁? 你知道Oracle 12c是否改变了这一点?我看到它标记为“在 ODAC 12c 或更高版本中可用”。这里:apex.oracle.com/pls/apex/…【参考方案2】:

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

这是一个很大的“如果”,但是,根据您的示例,父进程创建的连接不能由子进程共享(您在调用子进程之前不要关闭/释放连接)。这将导致跨越两个实际连接的事务,这将导致事务被提升为分布式事务。

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

【讨论】:

你是说如果我通过connection对象,需要在parent中关闭,在child中重新打开? 没有。我假设您正在尝试利用 TransactionScope 的便利性而不导致事务被提升为分布式事务。听起来这在以 Oracle 数据库为目标时是不可能的,但它可以以 SQL Server 数据库为目标……只要连接可以被池化并且一次只有一个打开的连接。 很高兴知道这一点。无论如何,在使用下一个之前关闭DbConnection 是一个好习惯,如果这样可以可靠地避免分布式事务(假设连接字符串相同),那就更好了!【参考方案3】:

在您的示例中,TransactionScope 仍然在方法的上下文中,您可以简单地创建一个 SqlTransaction 并在其下使用多个命令。如果您想将事务移出方法,例如该方法的调用者,或者如果您要访问多个数据库,请使用 TransactionScope。

更新:没关系,我刚刚发现了孩子的电话。在这种情况下,您可以将连接对象传递给子类。此外,您不需要手动处置 TransactionScope - 使用块就像 try-finally 块一样,即使出现异常也会执行处置。

更新 2: 更好的是,将 IDbTransaction 传递给子类。可以从中检索连接。

【讨论】:

是的,我知道没有明确需要处理,但是当我将 TransactionScope 改装到我的代码时,显然我已经忘记并继续将旧的 trm.Rollback 语句转换为 ts . 不假思索地处置。好地方! Re Update 2 - 您可以将IDbConnection 传递给子类。如果您使用IbConnection.BeginTransaction(),所有使用IDbConnection.CreateCommand() 创建的命令将自动关联事务。这样做更好,因为它减少了传递层次结构的一个参数,并且减少了耦合,因为子类不需要关心它们是否在事务中执行。

以上是关于TransactionScope:避免分布式事务的主要内容,如果未能解决你的问题,请参考以下文章

使用TransactionScope做分布式事务协调

ADO.NET中的TransactionScope何时需要启用MSTDC(分布式事务管理)

分布式事务TransactionScope所导致几个坑

关于分布式事务的一个误解:使用了TransactionScope就一定会开启分布式事务吗?

探索逻辑事务 TransactionScope

TransactionScope 事务 = new TransactionScope() VS TransactionScope s = context.Connection.BeginTransac