c# - TransactionScope helper ,无误地耗尽连接池 - 帮助?

标签 c# sql-server ado.net connection-pooling transactionscope

前阵子我问了一个关于 TransactionScope 升级到 MSDTC 的问题,而我并没有预料到它会这样。 ( Previous question )

归根结底,在 SQL2005 中,为了使用 TransactionScope,您只能在 TransactionScope 的生命周期内实例化并打开一个 SqlConnection。使用 SQL2008,您可以实例化多个 SqlConnections,但在任何给定时间只能打开一个。 SQL2000 将始终升级为 DTC...顺便说一句,我们的应用程序(WinForms 应用程序)不支持 SQL2000。

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

下面我附上了 LTS 代码的精简版本和示例控制台应用程序类,它们将演示连接池耗尽。为了观察您的连接池膨胀,请使用 SQL Server Managment Studio 的“事件监视器”或此查询:

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 进行编译。

注意事项:打开和关闭 SqlConnection 的是根级 LTS,它始终在池中打开一个新连接。嵌套 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();
     }
 }

最佳答案

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

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

因此在 MSDN 示例中,连接被放置在范围内部之前范围完成。虽然您的代码不同,但它在范围完成后 处理连接。我不是 TransactionScope 及其与 SqlConnection 交互方面的专家(我知道 一些 事情,但你的问题非常深入)而且我找不到任何规范,什么是正确的模式。但我建议您重新访问您的代码并在最外层范围完成之前处理单例连接,类似于 MSDN 示例。

此外,我希望您确实意识到,当第二个线程进入您的应用程序时,您的代码将会崩溃。

关于c# - TransactionScope helper ,无误地耗尽连接池 - 帮助?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/2222924/

相关文章:

c# - 如何从 C# 连接到 SQLite 数据库文件?

c# - 将一个数组中数字的频率写入另一个数组

c# - Nuget packages.config 和特定版本

mysql - 在有或没有索引的情况下复制数据库?

c# - 为什么通过 ADO.NET 执行的存储过程花费的时间比 SQL Server Management Studio 长很多倍?

ADO.NET:ExecuteScalar 是否自动关闭连接?

c# - 鼠标滚轮输入无法统一识别

c# - EntityFramework 不创建表

mysql - 一对多数据库

sql - 在 Sybase ASE 中插入多行