我在任何地方都找不到答案。
我将展示一个简单的代码片段,介绍如何轻松损坏连接池
连接池损坏意味着每次打开的新连接尝试都将失败
要体验我们需要的问题:
- 处于分布式事务中
- 嵌套sqlconnection及其在其他sqlconnection和sqltransaction中的sqltransaction
- 执行回滚(显式或隐式-只是不提交(嵌套sqltransaction
当连接池损坏时,每个sqlConnection.Open((都会抛出其中一个:
- SqlException:不允许启动新请求,因为它应该带有有效的事务描述符
- SqlException:分布式事务已完成。在新事务或NULL事务中登记此会话
ADO.NET内部存在某种线程竞争。如果我将Thread.Sleep(10)
放在代码中的某个位置,它可能会将接收到的异常更改为第二个异常。有时它会在没有任何修改的情况下发生变化
如何繁殖
- 启用分布式事务协调器窗口服务(默认情况下已启用(
- 创建空控制台应用程序
- 创建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中使用块do:
sqlTransaction3.Rollback(); SqlConnection.ClearPool(sqlConnection3);
用TransactionScopes替换所有SqlTransactions(
TransactionScope
必须包装SqlConnection.Open()
(在嵌套块中使用来自外部块的sqlconnection
有趣的观察结果:
如果应用程序在连接池损坏后等待几分钟,则一切正常。所以连接池破裂只持续几分钟。
已附加调试器。当执行离开时,抛出使用块
SqlException: The ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION.
的外部sqltransaction。try ... catch ....
无法捕捉到该异常。
如何解决
这个问题使我的web应用程序几乎瘫痪(无法打开任何新的sql连接(
呈现的代码片段是从包含对第三方框架的调用的整个管道中提取的。我不能简单地更改代码。
- 有人知道到底出了什么问题吗
- 是ADO.NET错误吗
- 也许我(和一些框架…(做错了什么
我的环境(这似乎不是很重要(
- .NET Framework 4.5
- MS SQL Server 2012
我知道这个问题很久以前就被问过了,但我想我已经为任何仍然有这个问题的人找到了答案。
SQL中的嵌套事务并不像创建它们的代码结构中那样。
无论有多少嵌套事务,只有外部事务才重要。
为了使外部事务能够提交,内部事务必须提交,换句话说,如果内部事务提交,则内部事务没有效果——外部事务必须仍然提交才能完成事务。
但是,如果内部事务回滚,则外部事务将回滚到其开始。外部事务必须仍然回滚或提交,否则它仍然处于启动状态。
因此,在上述示例中,线
//do nothing, sqlTransaction3 is alread rolled back by sqlTransaction2
应该是
sqlTransaction2.Rollback();
除非有其他事务可以完成并因此完成外部事务。