在啥情况下 SqlConnection 会自动加入环境 TransactionScope 事务?
Posted
技术标签:
【中文标题】在啥情况下 SqlConnection 会自动加入环境 TransactionScope 事务?【英文标题】:Under what circumstances is an SqlConnection automatically enlisted in an ambient TransactionScope Transaction?在什么情况下 SqlConnection 会自动加入环境 TransactionScope 事务? 【发布时间】:2011-02-22 12:20:09 【问题描述】:SqlConnection 在事务中“登记”是什么意思?是不是意味着我在连接上执行的命令会参与到事务中?
如果是这样,在什么情况下 SqlConnection 会自动加入环境 TransactionScope 事务?
查看代码 cmets 中的问题。我对每个问题的答案的猜测跟在括号中的每个问题之后。
场景 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?)
【问题讨论】:
【参考方案1】:自从提出这个问题以来,我已经做了一些测试,并且我自己找到了大部分(如果不是全部)答案,因为没有其他人回答。如果我遗漏了什么,请告诉我。
Q1:连接会自动加入事务吗?
是的,除非在连接字符串中指定了enlist=false
。连接池找到一个可用的连接。可用连接是未在事务中登记或在同一事务中登记的连接。
Q2:如果我现在使用相同的连接字符串打开(并运行命令)第二个连接,那么第二个连接与第一个连接的关系是什么(如果有)?
第二个连接是独立连接,参与同一个事务。我不确定这两个连接上命令的交互,因为它们针对同一个数据库运行,但我认为如果同时在两个连接上发出命令,可能会发生错误:"Transaction context in use by another session"
之类的错误>Q3:第二个连接在当前事务范围内的自动登记是否会导致事务升级为分布式事务?
是的,它会升级为分布式事务,因此即使使用相同的连接字符串,征用多个连接也会导致它成为分布式事务,这可以通过检查@987654325 处的非空 GUID 来确认@。
*更新:我在某处读到这在 SQL Server 2008 中已修复,因此当两个连接使用相同的连接字符串时不使用 MSDTC(只要两个连接没有同时打开)。这允许您在事务中多次打开和关闭连接,这样可以通过尽可能晚地打开连接并尽快关闭它们来更好地利用连接池。
Q4:如果我现在开始在连接上执行命令,它会自动加入当前事务范围吗?
没有。在没有事务范围处于活动状态时打开的连接不会自动加入新创建的事务范围。
Q5:如果没有加入,我在连接上执行的命令现在会参与环境事务吗?
没有。除非您在事务范围内打开连接,或者在范围内征用现有连接,否则基本上没有事务。您的连接必须自动或手动加入事务范围,以便您的命令参与事务。
Q6:如果此连接上的命令没有参与当前事务,即使回滚当前事务范围,它们是否会被提交?
是的,不参与事务的连接上的命令在发出时提交,即使代码恰好在已回滚的事务范围块中执行。如果连接未在当前事务范围内登记,则它不参与事务,因此提交或回滚事务将不会影响在未登记在事务范围内的连接上发出的命令...如this guy found out。除非您了解自动登记过程,否则很难发现这一点:仅当连接在在活动事务范围内打开时才会发生。
Q7:上述方法是否在当前环境事务中显式登记预先存在的连接,以便我在连接上执行的命令现在参与环境事务?
是的。通过调用EnlistTransaction(Transaction.Current)
,可以在当前事务范围内显式登记现有连接。您还可以使用 DependentTransaction 在事务中的单独线程上获取连接,但是像以前一样,我不确定针对同一数据库的同一事务中涉及的两个连接如何交互......并且可能会发生错误,并且当然,第二个登记连接会导致事务升级为分布式事务。
Q8:如果在我调用上述方法时,现有连接已经加入到事务中,会发生什么情况?会不会报错?
可能会引发错误。如果使用了TransactionScopeOption.Required
,并且连接已经在事务范围事务中登记,则没有错误;实际上,没有为范围创建新事务,并且事务计数 (@@trancount
) 没有增加。但是,如果您使用TransactionScopeOption.RequiresNew
,则在尝试在新的事务范围事务中登记连接时会收到一条有用的错误消息:“连接当前已登记事务。完成当前事务并重试。”是的,如果您完成了登记连接的事务,您可以安全地在新事务中登记连接。
*更新:如果您之前在连接上调用了BeginTransaction
,当您尝试加入新的事务范围事务时会抛出一个稍微不同的错误:“无法加入事务,因为本地事务在连接进度。完成本地事务并重试。”另一方面,您可以在 SqlConnection
在事务范围事务中登记时安全地调用 BeginTransaction
,这实际上会使 @@trancount
增加一个,这与使用嵌套事务范围的 Required 选项不同,它确实不会导致它增加。有趣的是,如果您接着使用 Required
选项创建另一个嵌套事务范围,您将不会收到错误,因为已经有一个活动的事务范围事务没有任何变化(请记住 @@trancount
不会增加事务范围的事务已经处于活动状态,并且使用了Required
选项)。
Q9:如果现有连接已经在事务中登记,并且我没有调用上述方法来登记它,我对其执行的任何命令是否会参与其现有事务而不是当前事务范围?
是的。无论 C# 代码中的活动事务范围是什么,命令都参与了连接登记的任何事务。
【讨论】:
写完 Q8 的答案后,我意识到这些东西开始看起来像万智牌的规则一样复杂!除了这更糟,因为 TransactionScope 文档没有解释任何这些。 没有。我编辑帖子以澄清。我的理解是,无论 SQL Server 版本如何,同时打开两个连接总是会导致分布式事务。在 SQL 2008 之前,一次只打开一个连接,使用相同的连接字符串仍然会导致 DT,但在 SQL 2008 中,一次打开一个连接(永远不会同时打开两个)使用相同的连接字符串不会导致DT 为了澄清你对 Q2 的回答,如果这两个命令在同一个线程上按顺序执行,它们应该可以正常运行。 关于 SQL 2008 中相同连接字符串的第三季度提升问题,这里是 MSDN 引用:msdn.microsoft.com/en-us/library/ms172070(v=vs.90).aspx @Triynko 请在回答之前重复问题。【参考方案2】:干得好 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 被征用时,现在有 2 个连接被征用,因此必须将事务提升到 MSDTC。只能通过使用自动登记来避免这种提升。
(2) 在.Net 4.0 之前,我强烈建议设置"Transaction Binding=Explicit Unbind" in the connection string。此问题已在 .Net 4.0 中修复,完全不需要显式取消绑定。
(3) 滚动您自己的CommittableTransaction
并将Transaction.Current
设置为此与TransactionScope
所做的基本相同。这实际上很少有用,仅供参考。
(4) Transaction.Current
是线程静态的。这意味着Transaction.Current
仅设置在创建TransactionScope
的线程上。所以多个线程执行相同的TransactionScope
(可能使用Task
)是不可能的。
【讨论】:
我刚刚测试了这个场景,它似乎像你描述的那样工作。此外,即使您使用自动登记,如果您在打开第二个连接之前调用“SqlConnection.ClearAllPools()”,它也会升级为分布式事务。 如果这是真的,那么交易中只能有一个“真正的”连接。 在不升级到分布式事务的情况下打开、关闭和重新打开 TransactionScope 事务中登记的连接的能力实际上是连接池创建的一种错觉,这通常会使已处理的连接保持打开状态,并且如果重新打开以进行自动登记,则返回相同的确切连接。 所以你真正想说的是,如果你回避自动登记过程,那么当你在事务范围事务 (TST) 中重新打开一个新连接时,而不是连接池抓取正确的连接(最初在 TST 中登记的连接),它非常恰当地抓取了一个全新的连接,当手动登记时,会导致 TST 升级。 无论如何,这正是我在回答 Q1 时所暗示的,当时我提到除非在连接字符串中指定“Enlist=false”,否则它已入伍,然后谈到了池如何找到合适的连接。 就多线程而言,如果您访问我对 Q2 的回答中的链接,您会发现虽然 Transaction.Current 对于每个线程都是唯一的,但您可以轻松地在一个线程中获取参考线程并将其传递给另一个线程;但是,从两个不同的线程访问 TST 会导致非常具体的错误“另一个会话正在使用事务上下文”。要对 TST 进行多线程处理,您必须创建一个 DependantTransaction,但此时它必须是一个分布式事务,因为您需要第二个独立连接来实际运行同时命令和 MSDTC 来协调两者。【参考方案3】:我们看到的另一种奇怪的情况是,如果你构造一个EntityConnectionStringBuilder
,它将与TransactionScope.Current
混为一谈,并且(我们认为)加入交易。我们在调试器中观察到了这一点,其中TransactionScope.Current
的current.TransactionInformation.internalTransaction
在构造之前显示enlistmentCount == 1
,之后显示enlistmentCount == 2
。
为避免这种情况,请在内部构造它
using (new TransactionScope(TransactionScopeOption.Suppress))
并且可能超出您的操作范围(我们每次需要连接时都在构建它)。
【讨论】:
以上是关于在啥情况下 SqlConnection 会自动加入环境 TransactionScope 事务?的主要内容,如果未能解决你的问题,请参考以下文章
在啥情况下 GetMsgProc 函数会收到小于 0 的代码?
AppDomain.DoCallback()在啥情况下会失败?