TransactionScope会破坏SqlConnection连接池吗?

6

我遇到了一个关于TransactionScope和异步/同步SQL调用的奇怪情况,很难理解。希望对这些操作有更深入了解的人能够解决这个问题。

情况: 我有一个NUnit测试固件,在[SetUp]期间创建TransactionScope,并在[TearDown]处释放它,以便每个测试都在相同的数据上运行。我有一系列的测试,其中会在数据库上启动异步操作,然后执行同步操作。第一个测试成功完成。第二个测试失败并显示"There is already an open DataReader associated with this Command which must be closed first."。

  1. 如果完全注释掉TransactionScope,则所有测试都通过。
  2. 我尝试了各种不同的TransactionScope选项和Complete/Dispose,但仍然出现相同的问题。
  3. 我正在使用Resharper测试运行器运行NUnit测试,.NET 4.5.1。
  4. 我知道“正确”的答案可能是“使一切都异步等待”。不幸的是,这对我来说不是一种选择。
  5. 我不想启用MARS,因为这个问题只在测试中出现。
  6. 由于潜在的死锁问题,我不想使用GetAwaiter().GetResult()。

在我的看法中,一旦调用TransactionScope.Dispose/Complete,自动SQLConnection池就会失去跟踪哪些连接有打开的DataReaders。它将同一个SqlConnection分配给两个同时运行的操作,并且第二个操作失败。

我主要的问题是“是什么导致了这种行为(具体来说是什么)?”
我的次要问题是“是否有任何安全的解决方法可以解决这个问题?”

下面的可复制代码打印出客户端连接Id。在我的机器上,第二个测试用例中的ASYNC和SYNC调用的ClientConnectionId始终相同。

可复制代码:

[TestFixture]
public class DataReaderTests
{
    private TransactionScope _scope;
    private string _connString = @"my connection string";

    [SetUp]
    public void Setup()
    {
        var options = new TransactionOptions()
        {
            IsolationLevel = IsolationLevel.ReadCommitted,
            Timeout = TimeSpan.FromMinutes(1)
        };
        _scope = new TransactionScope(TransactionScopeOption.RequiresNew, options, TransactionScopeAsyncFlowOption.Enabled);
    }

    [Test]
    [TestCase("First")]
    [TestCase("Second")]
    public void Test(string name)
    {
        DoAsyncThing().ConfigureAwait(false);
        using (var conn = new SqlConnection(_connString))
        {
            try
            {
                conn.Open();
                Console.WriteLine("SYNC: " + conn.ClientConnectionId);
                using (var cmd = conn.CreateCommand())
                {
                    cmd.CommandText = "SELECT 1";
                    using (var reader = cmd.ExecuteReader())
                    {
                        while (reader.Read())
                        {
                            int id = reader.GetInt32(0);
                        }
                    }
                }
            }
            catch (TransactionAbortedException tax)
            {
                Console.WriteLine("ERROR: " + ((SqlException)tax.InnerException.InnerException).ClientConnectionId);
                throw;
            }
        }
    }

    private async Task DoAsyncThing()
    {
        using (var connection = new SqlConnection(_connString))
        {
            await connection.OpenAsync();
            Console.WriteLine("ASYNC: " + connection.ClientConnectionId);
            using (var cmd = connection.CreateCommand())
            {
                cmd.CommandText = "WAITFOR DELAY '00:02';";
                await cmd.ExecuteNonQueryAsync();
                Console.WriteLine("ASYNC COMPLETE");
            }
        }
    }

    [TearDown]
    public void Teardown()
    {
        _scope.Dispose();            
    }
}`

1
你是否了解在事务中注册的SqlConnection的特殊连接池行为,详见此处:https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/sql-server-connection-pooling。这肯定是出乎意料的,并且可能不支持在单个事务中同时访问连接池。 - David Browne - Microsoft
1个回答

0

看看这个答案

我认为要点是,如果没有特殊的连接字符串属性,您不能同时在同一连接上执行两个活动的SQL命令。当您在事务范围内操作时,您应该发现两个SqlConnection对象具有相同的客户端ID。但是,如果您删除事务范围,则它们是不同的,这意味着它们正在使用不同的连接。

将“MultipleActiveResultSets=true”添加到连接字符串中可以解决我的问题。另一种选择是替换

DoAsyncThing().ConfigureAwait(false); 

带着

DoAsyncThing().ConfigureAwait(false).GetAwaiter().GetResult();

这将在开始第二个命令之前终止第一个命令。


根据is-getawaiter-getresult-safe-...和其他地方的信息,GetAwaiter().GetResult()可能会导致死锁,所以你是正确的 - 但我不能采取这种方法。MARS也可以防止错误,但这也不是我能够在生产代码中使用的东西(特别是对于单元测试问题)。我应该在我的帖子中加入这一点,现在我会这样做。 - Concerned Citizen

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