【发布时间】: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