TransactionScope 打破 SqlConnection 池?

Posted

技术标签:

【中文标题】TransactionScope 打破 SqlConnection 池?【英文标题】:TransactionScope breaking SqlConnection pooling? 【发布时间】:2018-11-21 05:09:31 【问题描述】:

我对 TransactionScope 和异步/同步 SQL 调用有一个奇怪的情况,我很难理解。我希望对这些操作的来龙去脉有更深入了解的人可以对这个问题有所了解。

情况: 我有一个 NUnit testfixture,它在 [SetUp] 期间创建一个 TransactionScope 并在 [TearDown] 处处理它,以让每个测试在相同的数据上运行。我有一系列测试,它们在数据库上启动异步操作,然后在数据库上执行同步操作。第一次这样的测试成功完成。第二个这样的测试失败,“已经有一个打开的 DataReader 与此命令关联,必须先关闭。”。

    如果我完全注释掉 TransactionScope,则所有测试都通过。 我尝试了各种不同的 TransactionScope 选项和 Complete / Dispose,但出现了同样的问题。 我在 NUnit 测试 .NET 4.5.1 上使用 Resharper 测试运行程序。 我意识到“正确”的答案可能是“让一切异步等待”。不幸的是,这不是我的选择。 我不想启用 MARS,因为此问题仅在测试中出现。 由于潜在的死锁,我不想使用 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();            
    
`

【问题讨论】:

您是否知道事务中登记的 SqlConnections 的特殊连接池行为,如下所述:docs.microsoft.com/en-us/dotnet/framework/data/adonet/…。这肯定是出乎意料的,并且可能不支持在单个事务中同时访问连接池。 【参考方案1】:

看看这个answer

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

将“MultipleActiveResultSets=true”添加到连接字符串为我解决了这个问题。另一种选择是替换

DoAsyncThing().ConfigureAwait(false); 

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

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

【讨论】:

根据is-getawaiter-getresult-safe-... 和其他地方, GetAwaiter().GetResult() 可能会导致死锁,所以你是对的 - 但我不能采取这种方法。 MARS 也将防止错误,但我也不能将其放入生产代码中(尤其是对于单元测试问题)。我应该把它放在我的帖子里,我现在就这样做。

以上是关于TransactionScope 打破 SqlConnection 池?的主要内容,如果未能解决你的问题,请参考以下文章

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

为啥 TransactionScope 不能与 Sqlite 一起使用?

如何加入 TransactionScope?

TransactionScope 是如何工作的?

探索逻辑事务 TransactionScope

忽略特定查询的 TransactionScope