我无法在任何地方找到答案。
我将展示一个简单的代码片段,演示如何轻松损坏连接池。
连接池损坏意味着每次尝试打开新连接都会失败。
我们需要以下条件来体验这个问题:
- 处于分布式事务中
- 嵌套sqlconnection和它的sqltransaction在其他sqlconnection和sqltransaction中
- 回滚(显式或隐式-只是不提交)嵌套sqltransaction
当连接池被破坏时,每个sqlConnection.Open()都会抛出以下异常之一:
- SqlException: 新请求不允许启动,因为它应该带有有效的事务描述符。
- SqlException: 分布式事务已完成。请在新事务中注册此会话,或使用NULL事务。
在ADO.NET中存在某种类型的线程竞争。如果在代码中的某个位置放置Thread.Sleep(10)
,它可能会将接收到的异常更改为第二个异常。有时会在没有任何修改的情况下进行更改。
如何重现
- 启用分布式事务协调器Windows服务(默认情况下启用)。
- 创建空控制台应用程序。
- 创建2个数据库(可以为空)或1个数据库并取消注释行:
Transaction.Current.EnlistDurable[...]
- 复制&粘贴以下代码:
var connectionStringA = String.Format(@"Data Source={0};Initial Catalog={1};Integrated Security=True;pooling=true;Max Pool Size=20;Enlist=true",
@".\YourServer", "DataBaseA");
var connectionStringB = String.Format(@"Data Source={0};Initial Catalog={1};Integrated Security=True;pooling=true;Max Pool Size=20;Enlist=true",
@".\YourServer", "DataBaseB");
try
{
using (var transactionScope = new TransactionScope())
{
//we need to force promotion to distributed transaction:
using (var sqlConnection = new SqlConnection(connectionStringA))
{
sqlConnection.Open();
}
// you can replace last 3 lines with: (the result will be the same)
// Transaction.Current.EnlistDurable(Guid.NewGuid(), new EmptyIEnlistmentNotificationImplementation(), EnlistmentOptions.EnlistDuringPrepareRequired);
bool errorOccured;
using (var sqlConnection2 = new SqlConnection(connectionStringB))
{
sqlConnection2.Open();
using (var sqlTransaction2 = sqlConnection2.BeginTransaction())
{
using (var sqlConnection3 = new SqlConnection(connectionStringB))
{
sqlConnection3.Open();
using (var sqlTransaction3 = sqlConnection3.BeginTransaction())
{
errorOccured = true;
sqlTransaction3.Rollback();
}
}
if (!errorOccured)
{
sqlTransaction2.Commit();
}
else
{
//do nothing, sqlTransaction3 is alread rolled back by sqlTransaction2
}
}
}
if (!errorOccured)
transactionScope.Complete();
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
那么:
for (var i = 0; i < 10; i++) //all tries will fail
{
try
{
using (var sqlConnection1 = new SqlConnection(connectionStringB))
{
// Following line will throw:
// 1. SqlException: New request is not allowed to start because it should come with valid transaction descriptor.
// or
// 2. SqlException: Distributed transaction completed. Either enlist this session in a new transaction or the NULL transaction.
sqlConnection1.Open();
Console.WriteLine("Connection successfully open.");
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
已知的不良解决方案以及值得注意的内容
不良解决方案:
在嵌套的 sqltransaction 使用块中执行以下操作:
sqlTransaction3.Rollback(); SqlConnection.ClearPool(sqlConnection3);
用 TransactionScopes 替换所有 SqlTransactions(
TransactionScope
必须包装SqlConnection.Open()
)在嵌套块中使用来自外部块的 sqlconnection
有趣的观察结果:
如果应用程序在连接池损坏后等待几分钟,那么一切都正常工作。 因此,连接池损坏只持续几分钟。
在附加了调试器的情况下。当执行离开外部 sqltransaction 使用块时,会抛出
SqlException: The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION.
。 这个异常无法通过try ... catch ....
来捕获。
如何解决?
这个问题使我的 web 应用程序几乎死机(无法打开任何新的 sql 连接)。
所提供的代码片段是从整个管道中提取的,其中包括对第三方框架的调用。我不能简单地更改代码。
- 有没有人知道具体出了什么问题?
- 这是 ADO.NET 的 bug 吗?
- 也许我(和一些框架……)做错了什么?
我的环境(似乎并不重要)
- .NET Framework 4.5
- MS SQL Server 2012