TransactionScope助手无故耗尽连接池 – 帮助?

前段时间我问了一个关于TransactionScope升级到MSDTC的问题,当时我还没想到。 ( 上一个问题 )

简而言之,在SQL2005中,为了使用TransactionScope,您只能在TransactionScope的生命周期内实例化并打开一个SqlConnection。 使用SQL2008,您可以实例化多个SqlConnections,但在任何给定时间只能打开一个SqlConnections。 SQL2000将始终升级到DTC …我们的应用程序中不支持SQL2000,一个WinForms应用程序,BTW。

我们对单连接问题的解决方案是创建一个名为LocalTransactionScope(又名’LTS’)的TransactionScope助手类。 它包装了一个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,以及一个示例控制台应用程序,您可以使用它来演示它将使用池中的连接并且永远不会重复使用也不会释放它们。 您需要为LTS添加对System.Transactions.dll的引用以进行编译。

注意事项:它是打开和关闭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(); } } /// 'Completes' the  this  encapsulates. public void Complete() { _TransactionScope.Complete(); } /// Creates a new  from the current  this  is managing. 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 randomTables = new List(); 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示例中, 范围完成之前 ,conenction将放置范围内。 您的代码虽然不同,但在范围完成会处理连接。 我不是TransactionScope及其与SqlConnection交互的专家(我知道一些事情,但你的问题非常深入)我找不到任何规范什么是正确的模式。 但我建议您重新访问代码并在最外层作用域完成之前处理单例连接,类似于MSDN示例。

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

这段代码合法吗?

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