Understanding nested transaction scopes

The TransactionScope object is central to the System.Transactions model.  At its simplest, it provides a very easy mechanism for demarcating transactions in the application — i.e.

 

using (TransactionScope s = new TransactionScope ())

{

s.Complete ();

}

 

However, hidden in that is the use of TransactionScopeOption, and the ability to nest TransactionScope regions.  This is one area that I’ve gotten a number of questions on, including the one that showed up in the Suggestion Box.

 

Often the first question is whether or not these are ‘nested transactions’, in which partial areas of the overall transaction may be rolled back, updated isolated between concurrent subtransactions, and still result in a single commit point for the overall transaction.  No, nested TransactionScopes in .Net 2.0 do not provide that.

 

That then leads into questions about what nesting TransactionScopes mean.  When we thought about this mechanism, we realized that there are two questions that an application developer has at the point that they would be creating a TransactionScope:

 

  • How does the contained code handle errors: does it require a transaction rollback, does it assume that it does the compensation itself?
  • How does the contained code relate to its caller: can it reasonably participate in a caller’s transaction, or does it require its own?

 

This converts into the three TransactionScopeOption settings:

 

  • Required: the contained code depends on a transaction for atomicity, isolation, and compensation, and could be part of a broader transaction.
  • RequiresNew: the contained code depends on a transaction, but must be independently committed or rolled back.
  • Suppress: the contained code performs its own compensation, so must not be part of a transaction.

 

The first setting is the default, under the assumption that if you are creating a TransactionScope, you probably want a transaction.  In that case, the normal situation is one where the operation you’re about to do can be reasonably integrated into a transaction that already active.

 

The second setting is for cases where the contained code block does require a transaction for its consistency, and provides a feature that demands that it be separate from any transaction that might already be active.  One typical example would be a function that provides activity logging to, say, a database.  It may be implemented such that it required a transaction to provide consistency, but it couldn’t accept an outer rollback to undo the record of the attempted activity.

 

The final setting, Suppress, handles case where the contained code needs to be sure that it is not executing as part of any transaction.  This is fairly uncommon for a local operations — the only real case for this would be if the contained code was designed to handle its own compensation, yet used recoverable resources, such as SQL, to do its actions.

 

On the other hand, the one case I can see it normally being useful is if the code is calling a remote operation, either through COM+ or Indigo, that accepts an incoming transaction.

 

In that case, the caller could either suppress the current transaction, or create a new one. The latter potentially has a much different performance profile, since the new transaction would still be a distributed one.  The way to avoid that performance overhead, and retain the  feature that the called remote operation executed outside the current transaction, would be to suppress any transaction around the outbound call.

 

 

While this helps explain the TransactionScope creation options, the other big question is what does Complete mean?  When we show a standalone example, it is easy to confuse Complete with Commit — after all, since the TransactionScope instance is the only one involved, the transaction does commit when it is disposed.

 

However, the reality is slightly more subtle.  Complete means just what the name suggests — that the code within the TransactionScope has completed (successfully) all the operations it intended to do as part of the transaction.  Note that this is also why you can’t call Complete twice.  To do so would mean that at least one of the two calls wasn’t the last intended operation.

 

When a TransactionScope is disposed it first looks to see if it was completed successfully.  If it was not, the transaction is immediately rolled back.  If it was, and this was the TransactionScope that created the transaction in the first place, the transaction is committed.  Finally, in both cases, the current transaction is replaced with the value that was current when the TransactionScope was created.

 

Thus we have, for success:

 

// no transaction is active here

using (TransactionScope s = new TransactionScope ())

{

// transaction ‘a’ is active here

using (TransactionScope t = new TransactionScope
(TransactionScopeOption.RequiresNew))

{

// transaction ‘b’ is active here

 

t.Complete ();

} // the transaction from ‘s’ is put into Current.

 

// transaction ‘a’ is active here

using (TransactionScope u = new TransactionScope ())

{

// transaction ‘a’ is active here

u.Complete ();

} // the transaction from ‘s’ is put into Current.

 

// transaction ‘a’ is active here

s.Complete ();

}

// ‘a’ commits at this point

// no transaction is active here

 

For a failure case we have:

 

// no transaction is active here

using (TransactionScope s = new TransactionScope ())

{

// transaction ‘a’ is active here

using (TransactionScope t = new TransactionScope
TransactionScopeOption.RequiresNew))

{

// transaction ‘b’ is active here

 

t.Complete ();

}

 

// transaction ‘a’ is active here

using (TransactionScope u = new TransactionScope ())

{

// transaction ‘a’ is active here

//u.Complete ();

} // ‘a’ rolls back here,

//the transaction from ‘s’ is put into Current.

 

// transaction ‘a’ is current, but aborted here

s.Complete ();

}  // ‘a’ is already aborted, so is just removed at this point

// no transaction is active here

 

 

In both cases, TransactionScope t is for a completely different transaction from scopes s & u.  In the first case, the transaction behind scope u does not commit until its originating scope (scope s) completes.

 

In the second case scope u ends without issuing a Complete, so it is aborted at that point. However, when scope s is restored as the current scope, the aborted transaction is made current.  This ensures that any later activity in scope s is attempted under the correct transaction.

This entry was posted in DB. Bookmark the permalink.

1 条 Understanding nested transaction scopes 的回复

  1. Pingback: El maravilloso mundo de las Transacciones « El TecnoBaúl de Kiquenet

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s