【问题标题】:TransactionScope helper that exhausts connection pool without fail - help?TransactionScope 帮助器可以毫无故障地耗尽连接池 - 帮助?
【发布时间】:2010-02-08 16:07:29
【问题描述】:

不久前,我问了一个关于 TransactionScope 升级到 MSDTC 的问题,但我没想到会这样。 (Previous question)

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

我们对单连接问题的解决方案是创建一个 TransactionScope 帮助器类,称为 LocalTransactionScope(又名“LTS”)。它包装了一个 TransactionScope,最重要的是,它为我们的应用程序创建和维护了一个 SqlConnection 实例。好消息是,它有效——我们可以在不同的代码片段中使用 LTS,它们都加入了环境事务。非常好。问题是,每个创建的 root LTS 实例都会从连接池中创建并有效地终止一个连接。 “有效地杀死”我的意思是它将实例化一个 SqlConnetion,它将打开一个 new 连接(无论出于何种原因,它从不重用池中的连接),并且当该根 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();
     }
 }

【问题讨论】:

  • 我刚刚运行了你的代码,没有任何问题,我只看到一个与 SQL 的连接。
  • 为什么 _Connection 是静态的?这意味着它将为单个最终用户上的所有实例打开单个连接。此外,您似乎从未将连接释放回池,那么如何从池中拉出一个呢?我是否误解了您的代码?
  • @Josh 我希望我也能这么说。 10 位不同的开发人员耗尽了我们办公室的连接池,我的另一个开发者朋友这段代码也耗尽了他的连接池。
  • Nissan-Fan : 静态连接是 LTS 的全部要点。不同的代码片段可以实例化 LTS 以加入环境事务。由于 TransactionScope (TS) 的限制,只能实例化一个 SqlConnection。因为不同的代码需要参与全局事务,但需要使用单个连接,LTS 将 TS 包装起来,提供单个 SqlConnection。当第一个('root')LTS 最终被释放时,它会关闭/释放/使它实例化的 SqlConnection 为空。避免升级到 MSDTC。

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


【解决方案1】:

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

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

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

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

【讨论】:

  • 感谢您的评论 - 我在后脑勺有一种唠叨的感觉,因为“做 TS 很奇怪”可能是问题的原因。让我尝试调整 dispose 代码,看看它是否有所作为。在大多数情况下,我们的应用程序没有做任何需要我们对 DB 代码进行线程化的大量 DB 操作,但是您看到什么不是线程安全的? ...除了处置代码。 ;) 我应该指出,在我们实际的 IDiposable 实现中更加充实并且(我希望)是线程安全的。我们实现 IDisposable 更像这样:copypastecode.com/21859
  • 您的代码不是线程安全的 1) 按设计 因为多个线程可以具有独立的事务范围,并且它们都将共享一个唯一的 sqlconnection 和 2) 通过实现因为构造函数 _IsNested 和 _connection 检查/分配不是线程安全的。
  • 1) 完全正确。 . .我对此无能为力。这是一个 WinForms 客户端应用程序,没有太多关注其他线程的问题,并且 2)哦,伙计!感谢您指出了这一点。 :) 1) 仍然胜过整个事情。此外,不幸的是,单个 SqlConnection 是我们陷入困境的原因 - 我们必须只有一个 SqlConnection 否则 TransactionScope 将太容易升级到 DTC。
  • 天啊天啊!你修好了!我可以+5你的答案吗?你所描述的是正是的问题。因为根 LTS 在 之前 处理事务范围,所以它关闭连接会以某种方式破坏连接在连接池中的可用性。我正在从连接脚下拉出事务范围。老兄,非常感谢。
  • 这就是我不喜欢通过框架的整个 TransactionScope/SqlConnection 交互的地方:它仅在使用 exactly 作为示例时才有效,只要一个偏离了一点点它变得混乱。最糟糕的是,它不会以有意义且可操作的错误消息优雅地失败,它只是开始表现出 oddstrange
【解决方案2】:

这段代码合法吗?

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多