事务范围(TransactionScope)助手在不断耗尽连接池,怎么办? - 求助?

8

不久之前,我提出了一个关于TransactionScope升级到MSDTC的问题,而我并没有期望它会这样做。 (以前的问题)

问题的实质是,在SQL2005中,为了使用TransactionScope,您只能在TransactionScope的生命周期内创建和打开单个SqlConnection。在SQL2008中,您可以创建多个SqlConnection,但在任何给定时间只能打开一个。 SQL2000将始终升级到DTC……我们的应用程序不支持SQL2000,这是一个WinForms应用程序,BTW。

我们解决单连接问题的方法是创建一个TransactionScope帮助程序类,称为LocalTransactionScope (aka 'LTS')。它包装一个TransactionScope,并且最重要的是为我们的应用程序创建和维护一个单独的SqlConnection实例。好消息是,它有效 - 我们可以在不同的代码片段中使用LTS并且它们都加入了环境事务。非常好。问题是,每个 LTS实例都将从连接池创建并且有效地杀死一个连接。所谓的“有效杀死”是指它将实例化一个SqlConnetion,该连接将打开一个新的连接(出于某种原因,它从未重用来自池的连接),并且当该根LTS被处理时,它将关闭和处理SqlConnection,这应该释放连接以便可以重新使用,但是显然从未被重用。池膨胀直到达到最大值,然后当建立一个max-pool-size+1连接时,应用程序失败。

下面我附上了LTS代码的简化版本和一个演示连接池耗尽的样例控制台应用程序类。为了观察连接池膨胀,请使用SQL Server Managment Studio的“Activity Monitor”或此查询:

SELECT DB_NAME(dbid) as 'DB Name',
COUNT(dbid) as 'Connections'
FROM sys.sysprocesses WITH (nolock)
WHERE dbid > 0
GROUP BY dbid

我在这里附上LTS和一个示例控制台应用程序,您可以使用它来证明它会从池中消耗连接,而不会重复使用或释放它们。您需要添加对System.Transactions.dll的引用以使LTS编译。
需要注意的是:根级别的LTS打开和关闭SqlConnection,总是在池中打开新连接。嵌套的LTS实例没有任何区别,因为只有根LTS实例建立SqlConnection。如您所见,连接字符串始终相同,因此它应该重新使用连接。
是否存在我们未满足的神秘条件导致连接无法被重复使用?除了完全关闭池外,是否有其他解决方案?
public sealed class LocalTransactionScope : IDisposable
{
      private static SqlConnection _Connection;    

      private TransactionScope _TransactionScope;
      private bool _IsNested;    

      public LocalTransactionScope(string connectionString)
      {
         // stripped out a few cases that need to throw an exception
         _TransactionScope = new TransactionScope();

         // we'll use this later in Dispose(...) to determine whether this LTS instance should close the connection.
         _IsNested = (_Connection != null);

         if (_Connection == null)
         {
            _Connection = new SqlConnection(connectionString);

            // This Has Code-Stink.  You want to open your connections as late as possible and hold them open for as little
            // time as possible.  However, in order to use TransactionScope with SQL2005 you can only have a single 
            // connection, and it can only be opened once within the scope of the entire TransactionScope.  If you have
            // more than one SqlConnection, or you open a SqlConnection, close it, and re-open it, it more than once, 
            // the TransactionScope will escalate to the MSDTC.  SQL2008 allows you to have multiple connections within a 
            // single TransactionScope, however you can only have a single one open at any given time. 
            // Lastly, let's not forget about SQL2000.  Using TransactionScope with SQL2000 will immediately and always escalate to DTC.
            // We've dropped support of SQL2000, so that's not a concern we have.
            _Connection.Open();
         }
      }

      /// <summary>'Completes' the <see cref="TransactionScope"/> this <see cref="LocalTransactionScope"/> encapsulates.</summary>
      public void Complete() { _TransactionScope.Complete(); }

      /// <summary>Creates a new <see cref="SqlCommand"/> from the current <see cref="SqlConnection"/> this <see cref="LocalTransactionScope"/> is managing.</summary>
      public SqlCommand CreateCommand() { return _Connection.CreateCommand(); }

      void IDisposable.Dispose() { this.Dispose(); }

      public void Dispose()
      {
          Dispose(true); GC.SuppressFinalize(this);
      }

      private void Dispose(bool disposing)
      {
         if (disposing)
         {
            _TransactionScope.Dispose();
            _TransactionScope = null;    

            if (!_IsNested)
            {
               // last one out closes the door, this would be the root LTS, the first one to be instanced.
               LocalTransactionScope._Connection.Close();
               LocalTransactionScope._Connection.Dispose();    

               LocalTransactionScope._Connection = null;
            }
         }
      }
   }

这是一个展示连接池耗尽的 Program.cs 文件:
class Program
{
      static void Main(string[] args)
      {
         // fill in your connection string, but don't monkey with any pooling settings, like
         // "Pooling=false;" or the "Max Pool Size" stuff.  Doesn't matter if you use 
         // Doesn't matter if you use Windows or SQL auth, just make sure you set a Data Soure and an Initial Catalog
         string connectionString = "your connection string here";

         List<string> randomTables = new List<string>();
         using (var nonLTSConnection = new SqlConnection(connectionString))
         using (var command = nonLTSConnection.CreateCommand())
         {
             command.CommandType = CommandType.Text;
             command.CommandText = @"SELECT [TABLE_NAME], NEWID() AS [ID]
                                    FROM [INFORMATION_SCHEMA].TABLES]
                                    WHERE [TABLE_SCHEMA] = 'dbo' and [TABLE_TYPE] = 'BASE TABLE'
                                    ORDER BY [ID]";

             nonLTSConnection.Open();
             using (var reader = command.ExecuteReader())
             {
                 while (reader.Read())
                 {
                     string table = (string)reader["TABLE_NAME"];
                     randomTables.Add(table);

                     if (randomTables.Count > 200) { break; } // got more than enough to test.
                 }
             }
             nonLTSConnection.Close();
         }    

         // we're going to assume your database had some tables.
         for (int j = 0; j < 200; j++)
         {
             // At j = 100 you'll see it pause, and you'll shortly get an InvalidOperationException with the text of:
             // "Timeout expired.  The timeout period elapsed prior to obtaining a connection from the pool.  
             // This may have occurred because all pooled connections were in use and max pool size was reached."

             string tableName = randomTables[j % randomTables.Count];

             Console.Write("Creating root-level LTS " + j.ToString() + " selecting from " + tableName);
             using (var scope = new LocalTransactionScope(connectionString))
             using (var command = scope.CreateCommand())
             {
                 command.CommandType = CommandType.Text;
                 command.CommandText = "SELECT TOP 20 * FROM [" + tableName + "]";
                 using (var reader = command.ExecuteReader())
                 {
                     while (reader.Read())
                     {
                         Console.Write(".");
                     }
                     Console.Write(Environment.NewLine);
                 }
             }

             Thread.Sleep(50);
             scope.Complete();
         }

         Console.ReadKey();
     }
 }

我刚刚顺利地运行了你的代码,并且只看到了一次连接到 SQL。 - JoshBerke
2
为什么_Connection是静态的?这意味着它将为单个终端用户的所有实例打开一个连接。此外,似乎您从未将连接释放回池中,那么如何从池中获取连接?我是否误解了您的代码? - Keith Adler
@Josh 我希望我能说同样的话。在我们办公室,有10个不同的开发人员用尽了连接池,我的另一个开发人员朋友也遇到了这个问题。 - Yoopergeek
Nissan-Fan:静态连接是LTS的全部重点。不同的代码片段可以实例化LTS以加入环境事务。由于TransactionScope(TS)的限制,只能实例化单个SqlConnection。因为不同的代码片段需要参与全局事务但需要使用单个连接,所以LTS包装了TS,并提供了单个SqlConnection。当第一个(“根”)LTS最终被处理时,它关闭/处理/置空了它实例化的SqlConnection。避免升级到MSDTC。 - Yoopergeek
2个回答

4

根据MSDN,预期的TransactionScope/SqlConnection模式是:

using(TransactionScope scope = ...) {
  using (SqlConnection conn = ...) {
    conn.Open();
    SqlCommand.Execute(...);
    SqlCommand.Execute(...);
  }
  scope.Complete();
}

在MSDN的示例中,连接是在作用域内被处理,而且是在作用域完成之前处理。但你的代码有所不同,它是在作用域完成之后处理连接。我对TransactionScope及其与SqlConnection的交互并不是很了解(我知道一些东西,但你的问题相当深入),我也找不到任何关于正确模式的规格说明。但我建议您重新审视代码,并在最外层作用域完成之前处理单例连接,类似于MSDN示例。
此外,我希望您意识到,一旦第二个线程进入您的应用程序,您的代码就会崩溃。

谢谢您的评论 - 我一直有一种难以摆脱的感觉,因为“做TS奇怪”的原因可能是问题的原因。让我尝试调整处理代码,看看是否有所改善。就大多数情况而言,我们的应用程序没有进行任何DB-heavy操作,因此不需要将DB代码线程化,但是除了处理代码之外,您看到了什么不是线程安全的呢? ;) 我应该指出,在我们实际的IDisposable实现中更加完善和(我希望)线程安全。我们的IDisposable实现更像这样:http://www.copypastecode.com/21859/ - Yoopergeek
1
你的代码不是线程安全的,原因如下:1)设计上,因为多个线程可以拥有独立的事务范围,但它们都会共享同一个唯一的 SQL 连接;2)实现上,因为构造函数中的_IsNested 和 _connection 检查/分配不是线程安全的。 - Remus Rusanu
  1. 绝对正确……我无法改变太多。由于这是一个 WinForms 客户端应用程序,所以不太需要考虑其他线程的问题。
  2. 哦,天啊!感谢您指出这一点。:) 然而,第一点仍然优于整个事情。此外,不幸的是,单个 SqlConnection 是我们陷入这种困境的原因——我们只能有一个 SqlConnection,否则 TransactionScope 很容易升级到 DTC。
- Yoopergeek
只要你理解1)并且接受它,2)就不是很重要 :) - Remus Rusanu
1
天啊,天啊,天啊!!! 你修好了!我能给你的答案加+5分吗?你所描述的问题完全就是问题所在。因为根LTS在关闭连接之前处理事务范围,某种程度上破坏了连接池中连接的可用性。我从连接的脚下拉出了事务范围。兄弟,非常感谢。 - Yoopergeek
3
这就是我不喜欢整个TransactionScope/SqlConnection框架交互的原因:只有当完全按照示例使用时才有效,一旦稍有偏差,它就会出现故障。最糟糕的是,它没有优雅地失败并提供有意义和可执行的错误信息,它只会开始表现得古怪和奇怪。 - Remus Rusanu

0
这段代码合法吗?
using(TransactionScope scope = ..)
{
    using (SqlConnection conn = ..)
    using (SqlCommand command = ..)
    {
        conn.Open();

        SqlCommand.Execute(..);
    }

    using (SqlConnection conn = ..) // the same connection string
    using (SqlCommand command = ..)
    {
        conn.Open();

        SqlCommand.Execute(..);
    }

    scope.Complete();
}

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