8

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

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

我们对单连接问题的解决方案是创建一个 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();
     }
 }
4

2 回答 2

4

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

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

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

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

于 2010-02-08T18:49:58.510 回答
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();
}
于 2010-07-15T07:34:08.207 回答