TransactionScope 助手会毫无失败地耗尽连接池 - 有帮助吗?

发布于 2024-08-20 20:30:50 字数 7146 浏览 12 评论 0原文

不久前,我问了一个关于 TransactionScope 升级到 MSDTC 的问题,当时我并没有预料到它会升级。 (上一个问题

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

我们对仅单连接问题的解决方案是创建一个 TransactionScope 帮助程序类,称为 LocalTransactionScope(又名“LTS”)。它包装了一个 TransactionScope,最重要的是,它为我们的应用程序创建并维护了一个 SqlConnection 实例。好消息是,它有效 - 我们可以在不同的代码段中使用 LTS,并且它们都加入环境事务。很不错。问题是,创建的每个 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();
     }
 }

A while back I asked a question about TransactionScope escalating to MSDTC when I wasn't expecting it to. (Previous question)

What it boiled down to was, in SQL2005, in order to use a TransactionScope, you can only instance and open a single SqlConnection within the life of the TransactionScope. With SQL2008, you can instance multiple SqlConnections, but only a single one can be open at any given time. SQL2000 will always escalate to DTC...we don't support SQL2000 in our application, a WinForms app, BTW.

Our solution to single-connection-only problem was to create a TransactionScope helper class, called LocalTransactionScope (aka 'LTS'). It wraps a TransactionScope and, most importantly, creates and maintains a single SqlConnection instance for our application. The good news is, it works - we can use LTS across disparate pieces of code and they all join the ambient transaction. Very nice. The trouble is, every root LTS instance created will create and effectively kill a connection from the connection pool. By 'Effectively Kill' I mean it will instance a SqlConnetion, which will open a new connection (for whatever reason, it never reuses a connection from the pool,) and when that root LTS is disposed, it closes and disposes the SqlConnection which is supposed to release the connection back to the pool so that it can be reused, however, it clearly never is reused. The pool bloats until it's maxed out, and then the application fails when a max-pool-size+1 connection is established.

Below I've attached a stripped down version of the LTS code and a sample console application class that will demonstrate the connection pool exhaustion. In order to watch your connection pool bloat, use SQL Server Managment Studio's 'Activity Monitor' or this query:

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

I'm attaching LTS here, and a sample console application that you can use to demonstrate for yourself that it will consume connections from the pool and never re-use nor release them. You will need to add a reference to System.Transactions.dll for LTS to compile.

Things to note: It's the root-level LTS that opens and closes the SqlConnection, which always opens a new connection in the pool. Having nested LTS instances makes no difference because only the root LTS instance establishes a SqlConnection. As you can see, the connection string is always the same, so it should be reusing the connections.

Is there some arcane condition we're not meeting that causes the connections not to be re-used? Is there any solution to this other than turning pooling off entirely?

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;
            }
         }
      }
   }

This is a Program.cs that will exhibit the connection pool exhaustion:

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();
     }
 }

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

烟酉 2024-08-27 20:30:50

根据 MSDN:

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

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

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

The expected TransactionScope/SqlConnection pattern is, according to MSDN:

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

So in the MSDN example the conenction is disposed inside the scope, before the scope is complete. Your code though is different, it disposes the connection after the scope is complete. I'm not an expert in matters of TransactionScope and its interaction with the SqlConnection (I know some things, but your question goes pretty deep) and I can't find any specifications what is the correct pattern. But I'd suggest you revisit your code and dispose the singleton connection before the outermost scope is complete, similarly to the MSDN sample.

Also, I hope you do realize your code will fall apart the moment a second thread comes to play into your application.

尘世孤行 2024-08-27 20:30:50

这段代码合法吗?

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();
}

Is this code legal?

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();
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文