在什么情况下 SqlConnection 会自动登记在环境 TransactionScope 事务中?
SqlConnection 在事务中“登记”意味着什么?这是否仅仅意味着我在连接上执行的命令将参与事务?
如果是这样,在什么情况下 SqlConnection 会自动加入环境 TransactionScope 事务中?
请参阅代码注释中的问题。我对每个问题答案的猜测在每个问题后面的括号中。
场景 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?)
}
What does it mean for an SqlConnection to be "enlisted" in a transaction? Does it simply mean that commands I execute on the connection will participate in the transaction?
If so, under what circumstances is an SqlConnection automatically enlisted in an ambient TransactionScope Transaction?
See questions in code comments. My guess to each question's answer follows each question in parenthesis.
Scenario 1: Opening connections INSIDE a transaction scope
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?)
}
Scenario 2: Using connections INSIDE a transaction scope that were opened OUTSIDE of it
//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?)
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
自从问这个问题以来,我做了一些测试,并找到了大多数(如果不是全部)答案,因为没有其他人回答。如果我错过了什么,请告诉我。
是的,除非在连接字符串中指定了
enlist=false
。连接池找到可用的连接。可用连接是指未在事务中登记的连接或在同一事务中登记的连接。第二个连接是独立连接,参与同一个事务。我不确定这两个连接上命令的交互,因为它们针对同一数据库运行,但我认为如果同时在两个连接上发出命令,可能会发生错误:类似 “另一个会话正在使用事务上下文”< /a>
是的,它会升级为分布式事务,因此即使使用相同的连接字符串,登记多个连接也会导致它成为分布式事务,这可以通过检查
Transaction 中的非空 GUID 来确认。 Current.TransactionInformation.DistributedIdentifier
。*更新:我在某处读到,此问题已在 SQL Server 2008 中修复,因此当两个连接使用相同的连接字符串时,不会使用 MSDTC(只要两个连接不同时打开)。这允许您在一个事务中多次打开连接并关闭它,这样可以通过尽可能晚地打开连接并尽快关闭连接来更好地利用连接池。
不会。在没有活动事务范围时打开的连接不会自动登记在新创建的事务范围中。
不会。除非您在事务范围中打开连接,或者在范围中登记现有连接,否则基本上没有事务。您的连接必须自动或手动登记在事务范围中,以便您的命令参与事务。
是的,不参与事务的连接上的命令会按发出时提交,即使代码恰好在已回滚的事务范围块中执行也是如此。如果连接未在当前事务范围内登记,则它不会参与事务,因此提交或回滚事务将不会对在未登记在事务范围内的连接上发出的命令产生任何影响...如 这个人发现了。除非您了解自动登记过程,否则这是一个非常难以发现的问题:只有在活动事务范围内打开连接时才会发生这种情况。
是的。可以通过调用
EnlistTransaction(Transaction.Current)
将现有连接显式登记到当前事务范围中。您还可以使用 DependentTransaction 在事务中的单独线程上登记连接,但像以前一样,我不确定针对同一数据库的同一事务中涉及的两个连接如何交互......并且可能会发生错误,并且当然,第二个登记连接会导致事务升级为分布式事务。可能会抛出错误。如果使用
TransactionScopeOption.Required
,并且连接已在事务范围事务中登记,则不会出现错误;事实上,没有为该范围创建新事务,并且事务计数 (@@trancount
) 不会增加。但是,如果您使用TransactionScopeOption.RequiresNew
,则在尝试在新事务范围事务中登记连接时,您会收到一条有用的错误消息:“连接当前已登记事务。完成当前事务并重试。 ”是的,如果您完成了连接所登记的事务,您可以安全地在新事务中登记该连接。*更新:如果您之前在连接上调用
BeginTransaction
,则当您尝试加入新的事务范围事务时,会引发略有不同的错误:“无法加入事务,因为本地事务正在连接中完成本地事务并重试。”另一方面,当SqlConnection
登记在事务范围事务中时,您可以安全地在SqlConnection
上调用BeginTransaction
,这实际上会增加@@trancount
> 增加一,这与使用嵌套事务范围的“Required”选项不同,后者不会导致其增加。有趣的是,如果您继续使用Required
选项创建另一个嵌套事务范围,您将不会收到错误,因为已经有一个活动的事务范围事务,因此不会发生任何变化(请记住>当事务范围事务已处于活动状态且使用
不会增加)。Required
选项时,@@trancount是的。命令参与连接所加入的任何事务,无论 C# 代码中的活动事务范围是什么。
I've done some tests since asking this question and found most if not all answers on my own, since no one else replied. Please let me know if I've missed anything.
Yes, unless
enlist=false
is specified in the connection string. The connection pool finds a usable connection. A usable connection is one that's not enlisted in a transaction or one that's enlisted in the same transaction.The second connection is an independent connection, which participates in the same transaction. I'm not sure about the interaction of commands on these two connections, since they're running against the same database, but I think errors can occur if commands are issued on both at the same time: errors like "Transaction context in use by another session"
Yes, it gets escalated to a distributed transaction, so enlisting more than one connection, even with the same connection string, causes it to become a distributed transaction, which can be confirmed by checking for a non-null GUID at
Transaction.Current.TransactionInformation.DistributedIdentifier
.*Update: I read somewhere that this is fixed in SQL Server 2008, so that MSDTC is not used when the same connection string is used for both connections (as long as both connections are not open at the same time). That allows you to open a connection and close it multiple times within a transaction, which could make better use of the connection pool by opening connections as late as possible and closing them as soon as possible.
No. A connection opened when no transaction scope was active, will not be automatically enlisted in a newly created transaction scope.
No. Unless you open a connection in the transaction scope, or enlist an existing connection in the scope, there basically is NO TRANSACTION. Your connection must be automatically or manually enlisted in the transaction scope in order for your commands to participate in the transaction.
Yes, commands on a connection not participating in a transaction are committed as issued, even though the code happens to have executed in a transaction scope block that got rolled back. If the connection is not enlisted in the current transaction scope, it's not participating in the transaction, so committing or rolling back the transaction will have no effect on commands issued on a connection not enlisted in the transaction scope... as this guy found out. That's a very hard one to spot unless you understand the automatic enlistment process: it occurs only when a connection is opened inside an active transaction scope.
Yes. An existing connection can be explicitly enlisted in the current transaction scope by calling
EnlistTransaction(Transaction.Current)
. You can also enlist a connection on a separate thread in the transaction by using a DependentTransaction, but like before, I'm not sure how two connections involved in the same transaction against the same database may interact... and errors may occur, and of course the second enlisted connection causes the transaction to escalate to a distributed transaction.An error may be thrown. If
TransactionScopeOption.Required
was used, and the connection was already enlisted in a transaction scope transaction, then there is no error; in fact, there's no new transaction created for the scope, and the transaction count (@@trancount
) does not increase. If, however, you useTransactionScopeOption.RequiresNew
, then you get a helpful error message upon attempting to enlist the connection in the new transaction scope transaction: "Connection currently has transaction enlisted. Finish current transaction and retry." And yes, if you complete the transaction the connection is enlisted in, you can safely enlist the connection in a new transaction.*Update: If you previously called
BeginTransaction
on the connection, a slightly different error is thrown when you try to enlist in a new transaction scope transaction: "Cannot enlist in the transaction because a local transaction is in progress on the connection. Finish local transaction and retry." On the other hand, you can safely callBeginTransaction
on theSqlConnection
while its enlisted in a transaction scope transaction, and that will actually increase@@trancount
by one, unlike using the Required option of a nested transaction scope, which does not cause it to increase. Interestingly, if you then go on to create another nested transaction scope with theRequired
option, you will not get an error, because nothing changes as a result of already having an active transaction scope transaction (remember@@trancount
is not increased when a transaction scope transaction is already active and theRequired
option is used).Yes. Commands participate in whatever transaction the connection is enlisted in, regardless of what the active transaction scope is in the C# code.
干得好,Triynko,你的答案对我来说看起来都非常准确和完整。我想指出的其他一些事情:
(1) 手动登记
在上面的代码中,您(正确地)显示了这样的手动登记:
但是,也可以这样做,使用连接字符串中的 Enlist=false。
这里还有一件事需要注意。当 conn2 打开时,连接池代码不知道您稍后要将其登记在与 conn1 相同的事务中,这意味着 conn2 被赋予与 conn1 不同的内部连接。然后,当 conn2 被登记时,现在有 2 个连接被登记,因此必须将事务提升到 MSDTC。只有使用自动入伍才能避免这种晋升。
(2) 在.Net 4.0之前,我强烈建议设置 连接字符串中的“事务绑定=显式解除绑定”。此问题在 .Net 4.0 中已得到修复,因此完全不需要显式解除绑定。
(3) 滚动您自己的
CommittableTransaction
并将Transaction.Current
设置为该值本质上与TransactionScope
相同做。这实际上很少有用,仅供参考。(4)
Transaction.Current
是线程静态的。这意味着Transaction.Current
仅在创建TransactionScope
的线程上设置。因此,多个线程执行同一TransactionScope
(可能使用Task
)是不可能的。Nice work Triynko, your answers all look quite accurate and complete to me. Some other things I would like to point out:
(1) Manual enlistment
In your code above, you (correctly) show manual enlistment like this:
However, it is also possible to do it like this, using Enlist=false in the connection string.
There is another thing to note here. When conn2 is opened, the connection pool code doesn't know that you want to later enlist it in the same transaction as conn1, which means that conn2 is given a different internal connection than conn1. Then when conn2 is enlisted, there are now 2 connections enlisted so the transaction must be promoted to MSDTC. This promotion can only be avoided by using automatic enlistment.
(2) Before .Net 4.0, I highly recommend setting "Transaction Binding=Explicit Unbind" in the connection string. This issue is fixed in .Net 4.0, making Explicit Unbind totally unnecessary.
(3) Rolling your own
CommittableTransaction
and settingTransaction.Current
to that is essentially the same thing as whatTransactionScope
does. This is rarely actually useful, just FYI.(4)
Transaction.Current
is thread-static. This means thatTransaction.Current
is only set on the thread that created theTransactionScope
. So multiple threads executing the sameTransactionScope
(possibly usingTask
) is not possible.我们看到的另一种奇怪的情况是,如果您构造一个
EntityConnectionStringBuilder
,它将与TransactionScope.Current
混在一起,并(我们认为)参与事务。我们在调试器中观察到了这一点,其中TransactionScope.Current
的current.TransactionInformation.internalTransaction
在构造之前显示enlistmentCount == 1
,并且之后enlistmentCount == 2
。为了避免这种情况,
请使用 (new TransactionScope(TransactionScopeOption.Suppress)) 在
内部构造它
并且可能超出您的操作范围(我们每次需要连接时都会构建它)。
One other bizarre situation we've seen is that if you construct an
EntityConnectionStringBuilder
it will muck withTransactionScope.Current
and (we think) enlist in the transaction. We've observed this in the debugger, whereTransactionScope.Current
'scurrent.TransactionInformation.internalTransaction
showsenlistmentCount == 1
before constructing, andenlistmentCount == 2
afterward.To avoid this, construct it inside
using (new TransactionScope(TransactionScopeOption.Suppress))
and possibly outside the scope of your operation (we were constructing it every time we needed a connection).