Помощник TransactionScope, который в обязательном порядке исчерпывает пул соединений - помогите? - PullRequest
8 голосов
/ 08 февраля 2010

Некоторое время назад я задал вопрос о расширении TransactionScope до MSDTC, когда я этого не ожидал. ( Предыдущий вопрос )

В SQL2005 все сводилось к тому, чтобы использовать TransactionScope, вы можете только создать экземпляр SqlConnection и открыть его в течение срока действия TransactionScope. В SQL2008 вы можете создавать несколько SqlConnections, но только один из них может быть открыт в любой момент времени. SQL2000 всегда будет преобразовываться в DTC ... мы не поддерживаем SQL2000 в нашем приложении, приложении WinForms, кстати.

Нашим решением проблемы единственного соединения было создание вспомогательного класса TransactionScope, который называется LocalTransactionScope (он же LTS). Он оборачивает TransactionScope и, самое главное, создает и поддерживает один экземпляр SqlConnection для нашего приложения. Хорошая новость в том, что это работает - мы можем использовать LTS для разных частей кода, и все они присоединяются к внешней транзакции. Очень хорошо. Проблема в том, что каждый созданный экземпляр root LTS будет создавать и эффективно уничтожать соединение из пула соединений. Под «эффективным уничтожением» я подразумеваю, что он будет создавать экземпляр SqlConnetion, который откроет новое соединение (по какой-либо причине оно никогда не использует соединение из пула), и когда этот корневой LTS удаляется, он закрывается и избавляется от SqlConnection, который должен освободить соединение обратно в пул, чтобы его можно было повторно использовать, однако он явно никогда не используется повторно. Пул раздувается до тех пор, пока не будет исчерпан, а затем приложение не будет работать, когда установлено соединение max-pool-size + 1.

Ниже я приложил урезанную версию кода LTS и пример класса консольного приложения, которое продемонстрирует исчерпание пула соединений. Чтобы наблюдать за раздутием пула соединений, используйте «Activity Monitor» 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 для компиляции.

Вещи, на которые следует обратить внимание: это 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();
     }
 }

Ответы [ 2 ]

4 голосов
/ 08 февраля 2010

Ожидаемый шаблон TransactionScope / SqlConnection, согласно MSDN :

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

Таким образом, в примере MSDN соединение расположено внутри области действия, до области действия завершено. Ваш код, хотя и отличается, он удаляет соединение после область действия завершена. Я не эксперт в вопросах TransactionScope и его взаимодействия с SqlConnection (я знаю некоторые вещи, но ваш вопрос идет довольно глубоко), и я не могу найти какие-либо спецификации, каков правильный шаблон. Но я бы посоветовал вам вернуться к своему коду и утилизировать одноэлементное соединение до завершения самой внешней области, аналогично примеру MSDN.

Кроме того, я надеюсь, что вы понимаете, что ваш код развалится в тот момент, когда в вашем приложении появится второй поток.

0 голосов
/ 15 июля 2010

Законен ли этот код?

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