通过嵌套的ADO.NET事务(带有MSDTC),连接池已被破坏

20

我无法在任何地方找到答案。

我将展示一个简单的代码片段,演示如何轻松损坏连接池。
连接池损坏意味着每次尝试打开新连接都会失败。

我们需要以下条件来体验这个问题:

  1. 处于分布式事务中
  2. 嵌套sqlconnection和它的sqltransaction在其他sqlconnection和sqltransaction中
  3. 回滚(显式或隐式-只是不提交)嵌套sqltransaction

当连接池被破坏时,每个sqlConnection.Open()都会抛出以下异常之一:

  • SqlException: 新请求不允许启动,因为它应该带有有效的事务描述符。
  • SqlException: 分布式事务已完成。请在新事务中注册此会话,或使用NULL事务。

在ADO.NET中存在某种类型的线程竞争。如果在代码中的某个位置放置Thread.Sleep(10),它可能会将接收到的异常更改为第二个异常。有时会在没有任何修改的情况下进行更改。


如何重现

  1. 启用分布式事务协调器Windows服务(默认情况下启用)。
  2. 创建空控制台应用程序。
  3. 创建2个数据库(可以为空)或1个数据库并取消注释行:Transaction.Current.EnlistDurable[...]
  4. 复制&粘贴以下代码:

var connectionStringA = String.Format(@"Data Source={0};Initial Catalog={1};Integrated Security=True;pooling=true;Max Pool Size=20;Enlist=true",
            @".\YourServer", "DataBaseA");
var connectionStringB = String.Format(@"Data Source={0};Initial Catalog={1};Integrated Security=True;pooling=true;Max Pool Size=20;Enlist=true",
            @".\YourServer", "DataBaseB");

try
{
    using (var transactionScope = new TransactionScope())
    {
        //we need to force promotion to distributed transaction:
        using (var sqlConnection = new SqlConnection(connectionStringA))
        {
            sqlConnection.Open();
        }
        // you can replace last 3 lines with: (the result will be the same)
        // Transaction.Current.EnlistDurable(Guid.NewGuid(), new EmptyIEnlistmentNotificationImplementation(), EnlistmentOptions.EnlistDuringPrepareRequired);

        bool errorOccured;
        using (var sqlConnection2 = new SqlConnection(connectionStringB))
        {
            sqlConnection2.Open();
            using (var sqlTransaction2 = sqlConnection2.BeginTransaction())
            {
                using (var sqlConnection3 = new SqlConnection(connectionStringB))
                {
                    sqlConnection3.Open();
                    using (var sqlTransaction3 = sqlConnection3.BeginTransaction())
                    {
                        errorOccured = true;
                        sqlTransaction3.Rollback();
                    }
                }
                if (!errorOccured)
                {
                    sqlTransaction2.Commit();
                }
                else
                {
                    //do nothing, sqlTransaction3 is alread rolled back by sqlTransaction2
                }
            }
        }
        if (!errorOccured)
            transactionScope.Complete();
    }
}
catch (Exception e)
{
    Console.WriteLine(e.Message);
}

那么:

for (var i = 0; i < 10; i++) //all tries will fail
{
    try
    {
        using (var sqlConnection1 = new SqlConnection(connectionStringB))
        {
            // Following line will throw: 
            // 1. SqlException: New request is not allowed to start because it should come with valid transaction descriptor.
            // or
            // 2. SqlException: Distributed transaction completed. Either enlist this session in a new transaction or the NULL transaction.
            sqlConnection1.Open();
            Console.WriteLine("Connection successfully open.");
        }
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);
    }
}


已知的不良解决方案以及值得注意的内容

不良解决方案:

  1. 在嵌套的 sqltransaction 使用块中执行以下操作:
    sqlTransaction3.Rollback(); SqlConnection.ClearPool(sqlConnection3);

  2. 用 TransactionScopes 替换所有 SqlTransactions(TransactionScope 必须包装 SqlConnection.Open()

  3. 在嵌套块中使用来自外部块的 sqlconnection

有趣的观察结果:

  1. 如果应用程序在连接池损坏后等待几分钟,那么一切都正常工作。 因此,连接池损坏只持续几分钟。

  2. 在附加了调试器的情况下。当执行离开外部 sqltransaction 使用块时,会抛出 SqlException: The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION.。 这个异常无法通过 try ... catch .... 来捕获。


如何解决?

这个问题使我的 web 应用程序几乎死机(无法打开任何新的 sql 连接)。
所提供的代码片段是从整个管道中提取的,其中包括对第三方框架的调用。我不能简单地更改代码。

  • 有没有人知道具体出了什么问题?
  • 这是 ADO.NET 的 bug 吗?
  • 也许我(和一些框架……)做错了什么?


我的环境(似乎并不重要)

  • .NET Framework 4.5
  • MS SQL Server 2012

1
我认为你的“不良方案”#2是正确的做法。我不确定为什么您更喜欢使用SqlTransaction而不是TransactionScope。 - Josh
1个回答

5

我知道这个问题很久以前就被问过了,但我认为我可以为仍然遇到此问题的人提供答案。

在 SQL 中,嵌套事务并不是按照创建它们的代码结构所表现的那样。

不管有多少个嵌套事务,只有外层事务才起作用。

为了让外层事务能够提交,内部事务必须先提交,换句话说,如果内部事务提交了,则没有效果 - 外部事务必须仍然提交才能完成事务。

但是,如果内部事务回滚,则外部事务将回滚到其开始的状态。 外部事务仍然必须回滚或提交 - 否则它仍处于开始状态

因此,在上面的示例中,该行

//do nothing, sqlTransaction3 is alread rolled back by sqlTransaction2

应该是

sqlTransaction2.Rollback();

除非存在其他可以完成并因此完成外部交易的交易。


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