在什么情况下,一个 SqlConnection 会自动注册到环境 TransactionScope 事务中?

208

SqlConnection被“enlisted”到事务中是什么意思?这是否意味着在该连接上执行的命令将参与到事务中?

如果是这样,什么情况下会自动将SqlConnection注册到环境TransactionScope Transaction中?

见代码注释中的问题。我对每个问题答案的猜测在每个问题后面用括号表示。

方案1:在事务范围内打开连接

using (TransactionScope scope = new TransactionScope())
using (SqlConnection conn = ConnectToDB())
{   
    // Q1: Is connection automatically enlisted in transaction? (Yes?)
    //
    // Q2: If I open (and run commands on) a second connection now,
    // with an identical connection string,
    // what, if any, is the relationship of this second connection to the first?
    //
    // Q3: Will this second connection's automatic enlistment
    // in the current transaction scope cause the transaction to be
    // escalated to a distributed transaction? (Yes?)
}

场景2:在事务范围内使用在其外打开的连接

//Assume no ambient transaction active now
SqlConnection new_or_existing_connection = ConnectToDB(); //or passed in as method parameter
using (TransactionScope scope = new TransactionScope())
{
    // Connection was opened before transaction scope was created
    // Q4: If I start executing commands on the connection now,
    // will it automatically become enlisted in the current transaction scope? (No?)
    //
    // Q5: If not enlisted, will commands I execute on the connection now
    // participate in the ambient transaction? (No?)
    //
    // Q6: If commands on this connection are
    // not participating in the current transaction, will they be committed
    // even if rollback the current transaction scope? (Yes?)
    //
    // If my thoughts are correct, all of the above is disturbing,
    // because it would look like I'm executing commands
    // in a transaction scope, when in fact I'm not at all, 
    // until I do the following...
    //
    // Now enlisting existing connection in current transaction
    conn.EnlistTransaction( Transaction.Current );
    //
    // Q7: Does the above method explicitly enlist the pre-existing connection
    // in the current ambient transaction, so that commands I
    // execute on the connection now participate in the
    // ambient transaction? (Yes?)
    //
    // Q8: If the existing connection was already enlisted in a transaction
    // when I called the above method, what would happen?  Might an error be thrown? (Probably?)
    //
    // Q9: If the existing connection was already enlisted in a transaction
    // and I did NOT call the above method to enlist it, would any commands
    // I execute on it participate in it's existing transaction rather than
    // the current transaction scope. (Yes?)
}
3个回答

191

我在提出这个问题后进行了一些测试,发现大部分甚至全部答案都可以自己找到,因为没有其他人回复。如果我漏掉了什么,请告诉我。

Q1:连接是否自动注册到事务中?

是的,除非在连接字符串中指定了 enlist=false。连接池会找到可用的连接。可用的连接是指未注册到事务中或已注册到相同事务中的连接。

Q2:如果我现在打开(并运行命令)第二个连接,使用相同的连接字符串,那么这个第二个连接与第一个连接有什么关系(如果有的话)?

第二个连接是独立的连接,参与相同的事务。由于它们正在针对同一数据库运行,所以我不确定这两个连接之间的命令交互,但我认为如果同时在两个连接上发出命令,则可能会出现错误,例如 "Transaction context in use by another session"

Q3:这个第二个连接自动加入当前事务范围,会导致事务升级为分布式事务吗?

是的,它会升级为分布式事务,因此即使使用相同的连接字符串,注册多个连接也会导致它变成分布式事务,可以通过检查 Transaction.Current.TransactionInformation.DistributedIdentifier 中的非空 GUID 来确认。

*更新:我在某个地方读到,这个问题在SQL Server 2008中已经得到解决,因此当相同的连接字符串用于两个连接时不使用MSDTC(只要两个连接不同时打开)。这允许您在事务中多次打开和关闭连接,通过尽可能晚地打开连接并尽快关闭连接来更好地使用连接池。

Q4:如果我现在开始在连接上执行命令,它会自动加入当前事务范围吗?

不会。在没有事务范围活动的情况下打开的连接将不会自动加入新创建的事务范围。

Q5:如果没有加入,我现在在连接上执行的命令会参与环境事务吗?

不会。除非您在事务范围内打开连接或将现有连接注册到范围内,否则基本上就没有事务。您的连接必须在事务范围内自动或手动注册才能使您的命令参与事务。

Q6:如果此连接上的命令未参与当前事务,则即使回滚当前事务范围,它们是否也会被提交?

是的,未参与事务的连接上的命令将被提交,即使代码恰好在已回滚的事务范围块中执行。如果连接没有在当前事务范围中注册,它就不参与该事务,因此提交或回滚事务对于在事务范围中未注册的连接发出的命令没有影响...正如这个人发现的。除非您了解自动注册过程,否则很难发现:它仅在在活动事务范围内打开连接时发生。
Q7:上述方法是否明确将预先存在的连接注册到当前环境事务中,以便我在连接上执行的命令现在参与环境事务?
是的。可以通过调用EnlistTransaction(Transaction.Current)显式将现有连接注册到当前事务范围中。您还可以使用DependentTransaction在单独的线程上将连接注册到事务中,但与之前一样,我不确定涉及相同数据库的两个连接在同一事务中可能会如何交互...可能会出现错误,并且当然第二个注册的连接会导致事务升级为分布式事务。
Q8:如果在调用上述方法时现有连接已经注册到事务中,会发生什么?可能会抛出错误吗?
可能会抛出错误。如果使用了TransactionScopeOption.Required,并且连接已经在事务范围事务中注册了,则不会出现错误;实际上,范围内没有创建新的事务,并且事务计数(@@trancount)不会增加。然而,如果您使用TransactionScopeOption.RequiresNew,那么当尝试将连接注册到新的事务范围事务时,您将收到一个有用的错误消息:"Connection currently has transaction enlisted. Finish current transaction and retry." 是的,如果您完成了连接所注册的事务,您就可以安全地将连接注册到新的事务中。更新:如果您之前在连接上调用了BeginTransaction,则在尝试注册到新的事务范围事务时会抛出略有不同的错误:"Cannot enlist in the transaction because a local transaction is in progress on the connection. Finish local transaction and retry." 另一方面,在事务范围事务中注册SqlConnection时,您可以安全地调用BeginTransaction,这将实际上使@@trancount增加1,与使用嵌套事务范围的Required选项不同,后者不会导致其增加。有趣的是,如果您随后继续使用Required选项创建另一个嵌套事务范围,您将不会收到错误,因为已经有一个活动的事务范围事务(请记住,当事务范围事务已经处于活动状态并使用Required选项时,@@trancount不会增加)。
如果现有连接已经在事务中注册,并且我没有调用上述方法将其注册,那么我在其上执行的任何命令都将参与其现有事务而不是当前事务范围吗?
是的。无论C#代码中的活动事务范围是什么,命令都会参与连接所注册的任何事务。

12
写完第8题的答案后,我意识到这个东西开始看起来像《万智牌》的规则一样复杂!不过这更糟糕,因为TransactionScope文档没有解释其中任何内容。 - Triynko
2
不,我正在编辑帖子以澄清。据我了解,无论SQL Server版本如何,同时打开两个连接始终会导致分布式事务。在SQL 2008之前,每次只打开一个具有相同连接字符串的连接仍将导致D.T.,但是在SQL 2008中,每次只打开一个(从未同时打开两个)具有相同连接字符串的连接将不会导致D.T。 - Triynko
1
为了澄清你对Q2的回答,如果它们在同一个线程上按顺序执行,这两个命令应该可以正常运行。 - Jared Moore
2
关于SQL 2008中相同连接字符串的Q3促销问题,这里是MSDN引用链接:http://msdn.microsoft.com/zh-cn/library/ms172070(v=vs.90).aspx - pseudocoder
1
@Triynko 请在回答前重复问题。 - Bruno Martinez
显示剩余4条评论

22
很棒Triynko,你的回答看起来非常准确和完整。我想指出一些其他的事情:
(1) 手动注册
在你上面的代码中,你(正确地)展示了手动注册的方式:
using (SqlConnection conn = new SqlConnection(connStr))
{
    conn.Open();
    using (TransactionScope ts = new TransactionScope())
    {
        conn.EnlistTransaction(Transaction.Current);
    }
}

然而,也有可能像这样做,使用连接字符串中的Enlist=false。
string connStr = "...; Enlist = false";
using (TransactionScope ts = new TransactionScope())
{
    using (SqlConnection conn1 = new SqlConnection(connStr))
    {
        conn1.Open();
        conn1.EnlistTransaction(Transaction.Current);
    }

    using (SqlConnection conn2 = new SqlConnection(connStr))
    {
        conn2.Open();
        conn2.EnlistTransaction(Transaction.Current);
    }
}

这里还有一件事情需要注意。当打开conn2时,连接池代码并不知道你想要在同一个事务中将其后续注册到conn1中,这意味着conn2会被赋予一个与conn1不同的内部连接。然后,当conn2被注册时,现在有两个连接已经注册了,因此必须将事务提升为MSDTC。只有使用自动注册才能避免此类提升。
(2) 在.Net 4.0之前,我强烈建议在连接字符串中设置"Transaction Binding = Explicit Unbind"。这个问题在.Net 4.0中得到了解决,因此Explicit Unbind完全是不必要的。
(3) 自己编写的CommittableTransaction并将Transaction.Current设置为它本质上与TransactionScope所做的是相同的。这很少实际上有用,只是供参考。
(4)Transaction.Current是线程静态的。这意味着只有创建TransactionScope的线程上才设置Transaction.Current。因此,多个线程执行相同的TransactionScope(可能使用Task)是不可能的。

我刚刚测试了这种情况,它似乎按照你所描述的方式工作。此外,即使您使用自动登记,如果在打开第二个连接之前调用 "SqlConnection.ClearAllPools()",那么它会升级为分布式事务。 - Triynko
所以你真正想说的是,如果你规避自动注册过程,那么当你在事务范围事务(TST)内重新打开一个新连接时,连接池不会抓取正确的连接(最初在TST中注册的连接),而是适当地抓取一个全新的连接,这样在手动注册后,会导致TST升级。 - Triynko
无论如何,这正是我在回答Q1时所暗示的内容,当我提到除非在连接字符串中指定了“Enlist=false”,否则它将被列入清单,然后讨论池如何找到合适的连接。 - Triynko
就多线程而言,如果您访问我在Q2答案中提供的链接,您将看到虽然Transaction.Current对于每个线程都是唯一的,但您可以轻松地在一个线程中获取引用并将其传递到另一个线程; 然而,从两个不同的线程访问TST会导致非常特定的错误“Transaction context in use by another session”。要使TST变成多线程,必须创建DependantTransaction,但此时它必须是分布式事务,因为您需要第二个独立连接来实际运行同时命令,并使用MSDTC协调这两个连接。 - Triynko
不,它们不会。第一个示例仅使用1个基础连接,而第二个示例使用2个基础连接,因此被提升为分布式(这通常是不好的,因为它更慢且更复杂)。以这种方式避免分布式事务是Enlist=true的目的。 - Jared Moore
显示剩余6条评论

1
我们还遇到了另一种奇怪的情况,如果您构建一个 EntityConnectionStringBuilder,它将干扰 TransactionScope.Current 并(我们认为)注册事务。我们在调试器中观察到这一点,在构建之前,TransactionScope.Currentcurrent.TransactionInformation.internalTransaction 显示 enlistmentCount == 1,而构建后则显示 enlistmentCount == 2
为了避免这种情况,请在以下范围内构建: using (new TransactionScope(TransactionScopeOption.Suppress)) 并且可能要在操作范围之外进行构建(我们每次需要连接时都会进行构建)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接