连接池被嵌套的ADO.NET事务损坏(使用MSDTC)

时间:2014-05-20 10:00:36

标签: transactions ado.net transactionscope msdtc

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

我将展示简单的代码片段,它介绍了如何轻松破坏连接池 连接池损坏意味着每个新连接打开尝试都将失败。

要解决我们需要的问题:

  1. 处于分布式交易中
  2. 嵌套的sqlconnection及其在sqlconnection和sqltransaction中的sqltransaction
  3. 做回滚(明确或暗示 - 根本不提交)嵌套的sqltransaction
  4. 当连接池损坏时,每个sqlConnection.Open()抛出以下一个:

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

    在ADO.NET中有一些线程竞争。如果我将Thread.Sleep(10)放在代码中的某个位置,它可以将收到的异常更改为第二个。有时它会随着任何修改而改变。


    如何重现

    1. 启用分布式事务处理协调器窗口服务(默认情况下已启用)。
    2. 创建空控制台应用程序。
    3. 创建2个数据库(可以为空)或1个数据库并取消注释行:Transaction.Current.EnlistDurable[...]
    4. 复制并粘贴以下代码:
    5. 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. 在嵌套块中使用外部块的SQL连接

      4. 有趣的观察:

        1. 如果apllication在连接池损坏后等待几分钟,那么一切正常。 所以连接池coruption只持续几分钟。

        2. 附加调试器。当执行离开使用块SqlException: The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION.的外部sqltransaction时抛出。 try ... catch ....无法捕获该异常。


        3. 如何解决?

          这个问题使我的Web应用程序几乎死了(无法打开任何新的SQL连接) 提出的代码片段是从整个管道中提取的,它也包含对第三方框架的调用。我不能简单地改变代码。

          • 有谁知道到底出了什么问题?
          • 是ADO.NET错误吗?
          • 也许我(和一些框架......)做错了什么?


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

          • .NET Framework 4.5
          • MS SQL Server 2012

1 个答案:

答案 0 :(得分:5)

我知道很久以前就问过这个问题了,但我想我仍然有问题的答案。

SQL中的嵌套事务不会像创建它们的代码结构中那样出现。

无论有多少嵌套交易,只有外部交易才重要。

对于能够提交的外部事务,内部事务必须提交,换句话说,内部事务在提交时不起作用 - 外部事务必须仍然提交以完成事务。

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

因此,在上面的例子中,行

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

应该是

sqlTransaction2.Rollback();

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