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

owe*_*kop 19 ado.net msdtc transactions transactionscope

我无处可寻.

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

要体验我们需要的问题:

  1. 在分布式交易中
  2. 嵌套的sqlconnection及其在sqlconnection和sqltransaction中的sqltransaction
  3. do rollback(explict或implict - 根本不提交)嵌套的sqltransaction

当连接池损坏时,每个sqlConnection.Open()抛出以下之一:

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

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


如何重现

  1. 启用分布式事务处理协调器窗口服务(默认情况下已启用).
  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);
}
Run Code Online (Sandbox Code Playgroud)

然后:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)


已知的糟糕解决方案和可以观察到的有趣内容

解决方案不好

  1. 使用块执行嵌套sqltransaction内部:
    sqlTransaction3.Rollback(); SqlConnection.ClearPool(sqlConnection3);

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

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

有趣的观察:

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

  2. 随附调试器.当执行离开外部sqltransaction时使用block SqlException: The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION.被抛出.这个例外是不能被捕获的try ... catch .....


怎么解决?

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

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


我的环境(看起来不是很重要)

  • .NET Framework 4.5
  • MS SQL Server 2012

Ste*_*ore 5

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

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

无论有多少嵌套事务,只有外部事务才对.

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

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

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

//do nothing, sqlTransaction3 is alread rolled back by sqlTransaction2
Run Code Online (Sandbox Code Playgroud)

应该

sqlTransaction2.Rollback();
Run Code Online (Sandbox Code Playgroud)

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