Did you
using (var scope = new TransactionScope(TransactionScopeOption.Required))
type in C #
using (var scope = new TransactionScope(TransactionScopeOption.Required))
? This means that the code running in the
using
block is in a transaction and after exiting this block, the changes will be committed or canceled. It sounds clear until you start digging deeper. And the deeper you dig, the "stranger and stranger" becomes. Anyway, on closer acquaintance with the
TransactionScope
class and, in general, .NET transactions, a whole lot of questions arose.
What is the
TransactionScope
class? As soon as we use the
using (var scope = new TransactionScope())
construct, everything in our program immediately becomes transactional? What is a “Resource Manager” and a “Transaction Manager”? Can I write my own resource manager and how does it “connect” to the
TransactionScope
instance created? What is a distributed transaction and is it true that a distributed transaction in SQL Server or Oracle Database is the same as a .NET distributed transaction?
In this publication, I tried to collect material that helps find answers to these questions and form an understanding of transactions in the .NET world.
Introduction
What are transactions and what problems do they solve?
The transactions in question are operations that transfer the system from one acceptable state to another and are guaranteed not to leave the system in an unacceptable state even in the event of unforeseen situations. What kind of acceptable states are, in general, depends on the context. Here we will consider an acceptable situation in which the data we process is complete. This implies that the changes that make up the transaction, all together or committed, or not made. In addition, changes to one transaction can be isolated from changes made to the system by another transaction. The basic requirements for transactions are indicated by the acronym ACID. For the first acquaintance with them, the
Wikipedia article will do.
')
A classic example of a transaction is the transfer of money between two accounts. In this situation, withdrawing money from account No. 1 without crediting to account No. 2 is unacceptable, in the same way as crediting to account No. 2 without withdrawing from account No. 1. In other words, we want both operations - both withdrawal and crediting - run immediately. If some of them fail, then the second operation should not be performed. You can call this principle "all or nothing." Moreover, it is desirable that operations are performed synchronously even in the event of system failures such as power outages, that is, we see the system in an acceptable state as soon as it becomes available after recovery.
In mathematical terms, we can say that there is an invariant with respect to the system, which we would like to preserve. For example, the amount on both accounts: it is necessary that after the transaction (money transfer) the amount remains the same as before it. By the way, in the classic example with the transfer of money, accounting also appears - the subject area, where the notion of a transaction naturally arose.
We illustrate the example of transferring money between two accounts. The first picture shows the situation when the transfer of 50 rubles from account number 1 to account number 2 was completed successfully. Green indicates that the system is in an acceptable state (the data is consistent).
Now let's imagine that the transfer is made outside the transaction and after the withdrawal of money from account No. 1 there was a failure, due to which the withdrawn money was not credited to account No. 2. The system would be in an unacceptable state (red).
If the error occurred between the withdrawal and crediting operations, but the transfer was carried out within a single transaction, the withdrawal operation will be canceled. As a result, the system will remain in its original acceptable state.
I will give examples of situations from the experience of our company in which transactions are useful: accounting for goods (accounting for the amount of goods of various kinds that are in certain stores and on the way), accounting for storage resources (accounting for the volume of the room occupied by goods of a certain type, volume of the room free to place the goods, the amount of goods that employees can move and automated storage systems for the day).
The problems that arise when data integrity is compromised are obvious. The information provided by the system does not just become unreliable - it loses touch with reality and turns into nonsense.
What transactions are considered here
Transaction benefits are known. So, to maintain data integrity, we need a relational database, because this is where transactions are made? Not really. It was said above that the concept of a transaction depends on the context, and now we will briefly consider which transactions we can talk about when discussing information systems.
To begin with, let's separate the concepts of subject domain transactions (business transactions) and system transactions. The latter can be implemented in different places and in different ways.
Let's enter from the highest level - the subject area. An interested person can declare that there are some acceptable states and he does not want to see the information system outside these states. Let's not invent unnecessary examples: transferring money between accounts is suitable here. Just to clarify that the transfer is not necessarily the transfer of money between the settlement accounts of two bank customers. Equally important is the task of accounting, when accounts must reflect the sources and purpose of the funds of the organization, and transfer - a change in the distribution of funds for these sources and destinations. This was an example of a
domain transaction .
Now let's see the most common and interesting examples of the implementation of system transactions. In system transactions, various technical means provide the requirements of the subject area. A classic, proven solution of this kind is
relational DBMS transactions (the first example). Modern database management systems (both relational
and not-so ) provide a transaction mechanism that allows you to either save (fix) all changes made during a specified period of work, or discard them (roll back). When using such a mechanism, the operations of withdrawing money from one account and crediting it to another account, which constitute the transaction of the subject area, will be combined with the means of the DBMS into a system transaction and either executed together or not at all.
Using the DBMS is, of course, not necessary. Roughly speaking, you can generally implement the DBMS transaction mechanism in your favorite programming language and enjoy the unstable and error-prone analogue of existing tools. But your “bicycle” can be optimized for specific situations in the subject area.
There are more interesting options. Modern industrial programming languages ​​(C # and Java in the first place) offer tools designed specifically for organizing transactions involving completely different subsystems, and not just a DBMS. In this publication we will call such transactions software. In the case of C #, these are
transactions from the System.Transactions namespace (second example), and this is described below.
Before moving on to the
System.Transactions
transaction, one cannot but mention another interesting phenomenon. Tools
System.Transactions
allow the programmer to independently implement the
software transactional memory . In this case, software operations that affect the state of the system (in the case of classical imperative programming languages, this is an assignment operation) are included by default in transactions that can be fixed and rolled back in much the same way as DBMS transactions. With this approach, the need to use synchronization mechanisms (in C # -
lock
, in Java -
synchronized
) is significantly reduced. A further development of this idea is a
software transactional memory supported at the platform level (third example). Such a miracle is expectedly found in a language whose elegance exceeds its industrial applicability, Clojure. And for worker-peasant languages ​​there are plug-in libraries that provide the functionality of software transactional memory.
System transactions can include several information systems, in which case they become distributed. Both DBMS transactions and software transactions can be distributed; it all depends on what kind of functionality the particular transaction implementation tool supports. Details of distributed transactions are discussed in the corresponding section. I will give a picture to make it easier to understand the items under discussion.
TL; DR by section
There are processes that consist of several indivisible (atomic) operations applied to the system, in general, not necessarily informational. Each indivisible operation can leave the system in an unacceptable state when the integrity of the data is violated. For example, if the transfer of money between two accounts is represented by two indivisible operations of withdrawing from account No. 1 and crediting to account No. 2, then performing only one of these operations will violate the integrity of the data. Money or disappear unknown where, or appear from nowhere. A transaction combines indivisible operations so that they are performed all together (of course, sequentially, if necessary) or not performed at all. You can talk about subject-matter transactions and transactions in technical systems that usually implement subject-matter transactions.
Transactions based on System.Transactions
What is it
In the .NET world, there is a software framework designed by the creators of a transaction management platform. From the perspective of the transaction programmer, this framework consists of the
TransactionScope
,
TransactionScopeOption
,
TransactionScopeAsyncFlowOption
and
TransactionOptions
of the
System.Transactions
namespace. If we talk about .NET Standard, then all this is available starting with
version 2.0 .
Transactions from the
System.Transactions
namespace are based on
the X / Open XA standard from The Open Group consortium . This standard introduces many of the terms discussed below, and, most importantly, describes distributed transactions, to which a special section is also devoted to this publication. Software transactions are also based on this standard in other platforms, such
as Java .
A typical transaction usage scenario for a C # programmer is as follows:
using (var scope = new System.Transactions.TransactionScope(System.Transactions.TransactionScopeOption.Required)) {
Inside the
using
block is placed the code that performs the work, the results of which should be recorded or canceled all together. Classic examples of such work are reading and writing to the database or sending and receiving messages from the queue. When the control leaves the
using
block, the transaction will be committed. If you remove the call to
Complete
, the transaction will be rolled back. Pretty simple.
It turns out that during a transaction rollback, all operations made inside such a
using
block will be canceled? And if I assigned some variable a different value, then this variable will restore the old value? When I first saw a similar design, I thought so. In fact, of course, not all changes will roll back, but only very
special ones . If all changes had been rolled back, then this would have been the software transactional memory described above. Now let's see what these particular changes are that can participate in software transactions based on
System.Transactions
.
Resource managers
In order for something to support transactions based on
System.Transactions
, it is necessary that it has information that transaction work is currently in progress and that it is registered in some register of participants in the transaction. You can get information about whether transaction work is going on by checking the static
Current
property of the
System.Transactions.Transaction
class. Entering a block
using
above view just sets this property if it has not been set before. And to register as a party to a transaction, you can use methods like
Transaction.Enlist Smth
. In addition, you need to implement the interface required by these methods. A resource manager (Resource Manager) is exactly that “something” that supports interaction with transactions from
System.Transactions
(a more specific definition is given below).
What are resource managers? If we work from C # with a DBMS, such as SQL Server or Oracle Database, we usually use the appropriate drivers, and they are the control resources. In the code, they are represented by the
System.Data.SqlClient.SqlConnection
and
Oracle.ManagedDataAccess.Client.OracleConnection
. Also,
they say MSMQ supports transactions based on
System.Transactions
. Guided by the knowledge and examples drawn from the Internet, you can create your own resource manager. The simplest example is in the next section.
In addition to resource managers, we must also have a transaction manager (Transaction Manager), who will monitor the transaction and timely give orders to the resource manager. Depending on which resource managers participate in the transaction (which characteristics they have and where they are located), different transaction managers are connected to the work. In this case, the selection of the appropriate version occurs automatically and does not require the intervention of a programmer.
More specifically, a resource manager is an instance of a class that implements the special interface
System.Transactions.IEnlistmentNotification
. An instance of the class, as instructed by the client, is registered as a participant in the transaction, using the static property
System.Transactions.Transaction.Current
. Later, the transaction manager calls the methods of the specified interface as necessary.
Clearly, the set of resource managers involved in a transaction may change at runtime. For example, after entering the
using
block, we can first do something in SQL Server, and then in Oracle Database. Depending on this set of resource managers, the transaction manager used is determined. To be more precise, the transaction protocol being used is determined by the set of resource managers, and the one who supports it is determined by the transaction manager based on the protocol. We will consider transaction protocols
later when we talk about distributed transactions. The mechanism for automatically selecting the appropriate transaction manager at run time when the resource managers involved in a transaction change is called Transaction Promotion.
Types of resource managers
Resource managers can be divided into two large groups: durable and non-permanent.
Durable Resource Manager is a resource manager that supports transactions even if the information system is unavailable (for example, when the computer is restarted). Non-permanent resource manager (Volatile Resource Manager) - a resource manager that does not support a transaction if the information system is unavailable. A non-persistent resource manager supports in-memory transactions only.
Classic durable resource managers are a DBMS (or a DBMS driver for a software platform). No matter what happens - even if the operating system fails, even if the power is turned off - the DBMS will guarantee the integrity of the data after it comes back to a working state. For this, of course, you have to pay some inconvenience, but in this article we will not consider them. An example of a non-persistent resource manager is the software transaction memory mentioned above.
Using TransactionScope
When creating an object of type
TransactionScope
you can specify some parameters.
First, there is a setting that tells the runtime environment what you need:
- use a transaction that already exists at the moment;
- be sure to create a new one;
- instead, execute code inside a
using
block outside of a transaction.
For all this, the
System.Transactions.TransactionScopeOption
enumeration is responsible.
Secondly, you can set the transaction isolation level. This is a parameter that allows you to find a compromise between the independence of change and the speed of work. The most independent level — serializable — ensures that there are no situations where changes made within one uncommitted transaction can be seen in another transaction. Each next level adds one such specific situation where simultaneously running transactions can affect each other.
By default, a transaction is opened on a serializable level, which can be unpleasant (see, for example, this comment ).Setting a transaction isolation level during creation TransactionScope
is a recommendation for resource managers. They may not even support all levels presented in the listing System.Transactions.IsolationLevel
. In addition, we must bear in mind that when using a connection pool for working with a database, the connection for which the transaction isolation level was changed will retain this level upon returning to the pool . Now, when the programmer gets this connection from the pool and relies on the default values, he will observe unexpected behavior.Typical work scenarios cTransactionScope
and significant pitfalls (namely, nested transactions) are well covered in this article on Habré .Applicability of software transactions
It should be said that in almost any information system that is in industrial operation, processes are launched that can lead the system to an unacceptable state. Therefore, it is necessary to control these processes, find out whether the current state of the system is acceptable, and, if not, restore it. Software transactions are a ready-made tool for keeping the system in acceptable condition.In each case it would be constructive to consider the cost:- integration of processes into the software transaction infrastructure (these processes need to be aware of
TransactionScope
and many other things); - maintaining this infrastructure (for example, the cost of renting equipment with Windows on board);
- employee training (since the topic of .NET transactions is not very common).
We should not forget that the transaction process may be required to report its progress to the "outside world", for example, to keep a log of actions outside the transaction.Obviously, the rejection of software transactions will require the creation or implementation of some other means of maintaining data integrity, which will also have its value. In the end, there are cases when data integrity problems are so rare that it is easier to restore the acceptable state of the system with surgical interventions than to maintain an automatic recovery mechanism.Example of non-permanent resource manager
Now consider an example of a simple resource manager that does not support recovery from a system failure. We will have a software transactional memory block that stores some value that can be read and rewritten. In the absence of a transaction, this block behaves like a normal variable, and in the presence of a transaction, it retains an initial value that can be restored after the transaction is rolled back. The code for such a resource manager is as follows: internal sealed class Stm<T> : System.Transactions.IEnlistmentNotification { private T _current; private T _original; private bool _enlisted; public T Value { get { return _current; } set { if (!Enlist()) { _original = value; } _current = value; } } public Stm(T value) { _current = value; _original = value; } private bool Enlist() { if (_enlisted) return true; var currentTx = System.Transactions.Transaction.Current; if (currentTx == null) return false; currentTx.EnlistVolatile(this, System.Transactions.EnlistmentOptions.None); _enlisted = true; return true; } #region IEnlistmentNotification public void Commit(System.Transactions.Enlistment enlistment) { _original = _current; _enlisted = false; } public void InDoubt(System.Transactions.Enlistment enlistment) { _enlisted = false; } public void Prepare(System.Transactions.PreparingEnlistment preparingEnlistment) { preparingEnlistment.Prepared(); } public void Rollback(System.Transactions.Enlistment enlistment) { _current = _original; _enlisted = false; } #endregion IEnlistmentNotification }
It can be seen that the only formal requirement is the implementation of the interface System.Transactions.IEnlistmentNotification
. From interesting things worth noting methods Enlist
(not part System.Transactions.IEnlistmentNotification
) and Prepare
. The method Enlist
just checks if the code works within the transaction, and, if so, registers an instance of its class as a non-permanent resource manager. The method Prepare
is invoked by the transaction manager before committing the changes. Our resource manager signals its readiness to commit by calling a method System.Transactions.PreparingEnlistment.Prepared
.The following is a code showing an example of using our resource manager: var stm = new Stm<int>(1); using (var scope = new System.Transactions.TransactionScope(System.Transactions.TransactionScopeOption.Required)) { stm.Value = 2; scope.Complete(); }
If immediately after exiting the block using
to read the property stm.Value
, then the set value is expected there 2
. And if you remove the call scope.Complete
, the transaction will be rolled back and the property stm.Value
will have the value 1
set before the transaction.Simplified sequence of calls when working with transactions is System.Transactions
shown in the diagram below.It can be seen that in this example not all the possibilities provided by the infrastructure are considered System.Transactions
. We will take a closer look at them after we become familiar with transactional protocols and distributed transactions in the next section.TL; DR by section
A programmer can use a class TransactionScope
to execute some code within an existing or new transaction. A transaction is committed if and only if a TransactionScope
method is called on an existing instance of a class Dispose
, despite the fact that before that it had calledComplete
. The programmer can indicate whether he wants to start a new transaction without fail, take advantage of an existing one or, conversely, execute a code outside of an existing transaction. Only resource managers are involved in the transaction - software components that implement certain functionality. Resource managers can be durable (recovering from a system failure) and non-permanent (non-recovering). A DBMS is an example of a durable resource manager. Coordination of resource managers is handled by a transaction manager — a software component that is automatically selected by the runtime environment without the participation of the programmer.A non-persistent resource manager is a class that implements the interface System.Transactions.IEnlistmentNotification
and in the method.Prepare
confirming its readiness to commit changes or, on the contrary, signaling a rollback of changes. When the caller does something with the resource manager, it checks to see if the transaction is open now, and, if open, is registered with the method System.Transactions.Transaction.EnlistVolatile
.Distributed transactions
What is it
A distributed transaction involves several information subsystems (in fact, not everything is so simple, below is described in more detail). It is understood that changes in all systems involved in a distributed transaction must either be committed or rolled back.Above, various means of transaction implementation were presented: the DBMS, the infrastructure System.Transactions
, the software transactional memory built into the platform. Distributed transactions can be provided with these tools. For example, in Oracle Database, changing (and actually reading) data in several databases within a single transaction automatically turns it into a distributed one. Then we will talk about software distributed transactions, which can include heterogeneous resource managers.Transactional Protocols
Transaction Protocol is a set of principles by which applications involved in a transaction interact. The following protocols are most common in the .NET world.Lightweight. A maximum of one durable resource manager is used. All transactional interactions occur within the same application domain, or the resource manager supports promotion and single-phase commit (implements IPromotableSinglePhaseNotification
).OleTx. Interworking between multiple application domains and multiple computers is allowed. The use of many durable resource managers is allowed. All participating computers must be running Windows. Uses remote procedure call (RPCs).WS-AT.Interworking between multiple application domains and multiple computers is allowed. The use of many durable resource managers is allowed. Participating computers can be running different operating systems, not just Windows. Hypertext Transmission Protocol (HTTP) is used.It was noted above that the current transactional protocol influences the choice of the transaction manager, and the choice of the protocol is influenced by the characteristics of the control resources involved in the transaction. Now we list the known transaction managers.Lightweight Transaction Manager (LTM) . Introduced in the .NET Framework 2.0 and later. Manages transactions using the Lightweight protocol.Kernel Transaction Manager (KTM). Introduced in Windows Vista and Windows Server 2008. Manages transactions using the Lightweight protocol. It can call a transactional file system (Transactional File System, TxF) and a transaction registry (Transactional Registry, TxR) in Windows Vista and Windows 2008.Distributed Transaction Coordinator (MSDTC) . Manages transactions using the OleTx and WS-AT protocols.It should also be borne in mind that some of the resource managers do not support all the protocols listed. For example, MSMQ and SQL Server 2000 do not support Lightweight, so transactions involving MSMQ or SQL Server 2000 will be managed by MSDTC, even if they are the only participants. Technically, this limitation arises from the fact that the specified resource managers, implementing, of course, the interfaceSystem.Transactions.IEnlistmentNotification
do not implement the interface System.Transactions.IPromotableSinglePhaseNotification
. In it there is, among other things, a method Promote
that the runtime environment calls, if necessary, to switch to a more “cool” transaction manager.Now the ambiguity of the concept of a distributed transaction should become apparent. For example, you can define a distributed transaction as a transaction in which it participates:- at least two any resource managers;
- as many non-permanent resource managers and at least two durable ones;
- at least two of any resource managers, necessarily located on different computers.
Therefore, it is always better to clarify exactly which transactions in question.And in this context, MSDTC is primarily discussed. It is a software component of Windows that manages distributed transactions. There is a graphical interface for configuration and monitoring of transactions, which can be detected in the Component Services utility by following the path “Computers - My Computer - Distributed Transaction Coordinator - Local DTC”.For configuration, select the “Properties” item in the context menu of the “Local DTC” node, and to monitor distributed transactions, select the “Transaction statistics” item in the central panel.Biphasic fixation
If several resource managers are involved in a transaction, the results of their work may differ: for example, one of them has succeeded, and he is ready to commit changes, while the other has an error, and he is going to roll back the changes. However, the essence of a distributed transaction lies precisely in the fact that the changes of all the controlling resources involved in the transaction are either fixed all together or rolled back. Therefore, in such cases, the two-phase fixation protocol is usually used.In general, the essence of this protocol is as follows. During the first phaseresource managers involved in the transaction prepare enough information to recover from a failure (if it is a durable resource manager) and to successfully complete the work as a result of committing. From a technical point of view, the resource manager signals that he has completed the first phase, calling the method System.Transactions.PreparingEnlistment.Prepared
in the method Prepare
. Or the resource manager can notify about the rollback of changes by calling the method ForceRollback
.When all involved in the transaction resource managers "voted", that is, notified the transaction manager about whether they want to commit or roll back the changes, the second phase begins. At this time, resource managers are instructed to record their changes (if all participants voted for fixation) or to refuse changes (if at least one participant voted for rollback). Technically, this is expressed in calling methods Commit
and methods Rollback
that implement resource managers and in which they call the method System.Transactions.Enlistment.Done
.The resource manager can call the method System.Transactions.Enlistment.Done
during the first phase. In this case, it is assumed that he is not going to record any changes (for example, works only for reading) and will not participate in the second phase. More details about the two-phase fixation can be found at Microsoft .If the connection between the transaction manager and at least one of the resource managers is lost, then the transaction becomes frozen (“in doubt”, in-doubt). The transaction manager, by invoking methods InDoubt
, notifies the available resource managers that can respond appropriately about this event.There is also a three-phase fixation and its modifications with its advantages and disadvantages. The three-phase commit protocol is less common, perhaps because it requires even more expenses for messages between interacting subsystems.Cheat Sheet Interfaces System.Transactions
Something difficult turns out. To sort things out a bit, I will briefly describe the main namespace interfaces System.Transactions
needed to create a resource manager. Here is the class diagram.IEnlistmentNotification. The resource manager must implement this interface. The transaction manager calls the implemented methods in the following order. During the first phase, he calls the method Prepare
(unless the stars came together to call the method ISinglePhaseNotification.SinglePhaseCommit
, as described in the next paragraph). As part of this method, the resource manager saves the information necessary for recovery after a failure, prepares for final fixation of changes on its side and votes for the fixation or rollback of changes. If there comes a second phase, depending on the availability of resources and control of the results of the vote Managing transactions is one of three methods: Commit
, InDoubt
, Rollback
.ISinglePhaseNotification.The resource manager implements this interface if it wants to provide the transaction manager with the ability to optimize execution by reducing the second phase of commit. If the transaction manager sees only one resource manager, then in the first fixation phase he tries to call the method SinglePhaseCommit
(instead of IEnlistmentNotification.Prepare
) to the resource manager and thereby eliminate the vote and the transition to the second phase. This approach has advantages and disadvantages, which are most clearly written by Microsoft here .ITransactionPromoter. The resource manager implements this interface (not directly, but through the interfaceIPromotableSinglePhaseNotification
), if it wants to provide the transaction manager with the ability to adhere to the Lightweight protocol even with a remote call, until other conditions arise that will require complication of the protocol. When the protocol needs to be complicated, a method will be called Promote
.IPromotableSinglePhaseNotification. The resource manager implements this interface in order, first, to implement the interface ITransactionPromoter
, and second, so that the transaction manager can use a single-phase commit by calling the IPromotableSinglePhaseNotification.SinglePhaseCommit
and methods IPromotableSinglePhaseNotification.Rollback
. The transaction manager calls the method IPromotableSinglePhaseNotification.Initialize
to mark the successful registration of the resource manager using the simplified scheme. More or less, this can be understood from the Microsoft document .A little more look at the classSystem.Transactions.Enlistment
and his heirs. Instances of this type provide a transaction manager when invoking interface methods implemented by a resource manager.Enlistment. The resource manager can call a single method of this type, - Done
, to signal the successful completion of its part of the work.PreparingEnlistment. Using an instance of this type during the first commit phase, the resource manager can signal its intention to commit or roll back the changes. A durable resource manager can also get the information required to recover from a system failure.SinglePhaseEnlistment. Using an instance of this type, the resource manager can transmit information to the transaction manager about the results of its work when using the simplified scheme (single-phase commit).Software Distributed Transaction Restrictions and Alternatives
A brief survey of opinions found on the Internet shows that in many areas distributed transactions are going out of fashion. Look, for example, at this spiteful comment . The main object of criticism, which is briefly described here , is the synchronous (blocking) nature of distributed transactions. If a user sent a request, during the processing of which a distributed transaction was organized, he will receive a response only after all subsystems included in the transaction have finished (or with an error). At the same time, there is an opinion supported by research that the two-phase commit protocol shows poor performance, especially with an increase in the number of subsystems involved in the transaction, which is mentioned, for example, inof this publication on "Habré" .If the creator of the system prefers to return the answer to the user as soon as possible, putting the reconciliation of the data for later, then some other solution is more suitable for him. In the context of the Brewer theorem ( CAP theorems ), it can be said that distributed transactions are suitable for cases where data consistency (Consistency) is more important than availability (Availability).There are other practical restrictions on the use of software distributed transactions. For example, it was experimentally established that distributed transactions using the OleTx protocol should not cross network domains. In any case, the long attempts to get them to work were not crowned with success. In addition, it was found that the interaction between several instances of the Oracle Database (distributed transaction DBMS) imposes serious restrictions on the applicability of software distributed transactions (again failed to start).What are the alternatives to distributed transactions? First, it must be said that it will be very difficult to do without technical transactions (ordinary, not distributed). There will surely be processes in the system that may temporarily violate the integrity of the data, and it will be necessary to somehow oversee such processes. Similarly, in terms of the subject area, a concept may arise that includes a process that is implemented by a set of processes in different technical systems, which should begin and end in the field of complete data.Turning to alternatives to distributed transactions, you can note solutions based on messaging services, such as RabbitMQ and Apache Kafka. In this publication on "Habré" four such solutions are considered:- , , ;
- , (Transaction Log Tailing);
- , ;
- (Event Sourcing).
Another alternative is the Saga template. It assumes a cascade of subsystems with its local transactions. Upon completion, each system calls the following (either independently or with the help of a coordinator). For each transaction, there is a corresponding cancellation transaction, and the subsystem, instead of transferring control further, on the contrary, initiates the cancellation of changes made by previous subsystems. On "Habré" there are several good articles about the template "Saga". For example, this publication provides general information about maintaining the ACID principles in the microservices, and in this article an example of the implementation of the Saga template with the coordinator is discussed in detail.In our company, some products successfully use software distributed transactions through WCF, but there are other options. One day, when we tried to make friends with a distributed transaction, we had many problems, including collisions with the limitations described above and parallel troubles with updating the software infrastructure. Therefore, in the conditions of a shortage of resources for running around another capital decision, we used the following tactics. The called party fixes the changes in any case, but notes that they are in the draft state, therefore these changes do not affect the work of the called system. Then the caller when completing his work through a distributed transaction DBMS activates the changes made by the called system. In this way,instead of software distributed transactions, we used distributed DBMS transactions, which in this case turned out to be much more reliable.So is it in .NET Core?
In .NET Core (and even in .NET Standard) there are all the necessary types for organizing transactions and creating your own resource manager. Unfortunately, transactions based on .NET Core System.Transactions
have a serious limitation: they only work with the Lightweight protocol. For example, if the code uses two long-lived resource managers, then at run-time, the environment will throw an exception as soon as the second manager is contacted.The fact is that .NET Core is being tried to be independent of the operating system, therefore the list of transaction managers such as KTM and MSDTC is excluded, and they are needed to support transactions with the specified properties. It is possible that the connection of transaction managers in the form of plug-ins will be implemented, but so far this has been written with water, so it’s impossible to count on the industrial use of distributed transactions in .NET Core.From experience, you can be sure of the differences in distributed transactions in the .NET Framework and in the .NET Core by writing the same code, compiling and running it on different platforms.An example of such a code that calls sequentially SQL Server and Oracle Database. private static void Main(string[] args) { using (var scope = new System.Transactions.TransactionScope(System.Transactions.TransactionScopeOption.Required)) { MsSqlServer(); Oracle(); scope.Complete(); } } private static void Oracle() { using (var conn = new Oracle.ManagedDataAccess.Client.OracleConnection("User Id=some_user;Password=some_password;Data Source=some_db")) { conn.Open(); using (var cmd = conn.CreateCommand()) { cmd.CommandText = "update t_hello set id_hello = 2 where id_hello = 1"; cmd.ExecuteNonQuery(); } conn.Close(); } } private static void MsSqlServer() { var builder = new System.Data.SqlClient.SqlConnectionStringBuilder { DataSource = "some_computer\\some_db", UserID = "some_user", Password = "some_password", InitialCatalog = "some_scheme", Enlist = true, }; using (var conn = new System.Data.SqlClient.SqlConnection(builder.ConnectionString)) { conn.Open(); using (var cmd = conn.CreateCommand()) { cmd.CommandText = "update t_hello set id_hello = 2 where id_hello = 1"; cmd.ExecuteNonQuery(); } conn.Close(); } }
Ready to build projects are on GitHub .Running the example for .NET Core ends in error. The location and type of the thrown exception depend on the order of the DBMS call, but in any case this exception indicates an invalid transactional operation. Running the example for the .NET Framework ends successfully if MSDTC is running at the time; in this case, in the MSDTC GUI, you can observe the registration of a distributed transaction.Distributed transactions and WCF
The Windows Communication Foundation (WCF) is the software framework of the .NET world for organizing and invoking network services. Compared with the more fashionable approaches of REST and ASP.NET Web API, it has its own advantages and disadvantages. WCF is very good at making .NET transactions, and in the world of the .NET Framework it is convenient to use it for organizing transactions distributed between a client and a service.In .NET Core, this technology works only on the client side, that is, you cannot create a service, but you can only access an existing one. This, however, is not very important, because, as mentioned above, with the distributed transactions in the .NET Core things are not very good at all.How WCF worksFor readers who are not familiar with WCF, here we will give the most brief background information about what this technology is in practice. Context - two information systems, called client and service. The client at runtime accesses another information system that supports the service of interest to the client, and requires that some operation be performed. Then control is returned to the client.To create a service on WCF, you usually need to write an interface that describes the contract of the service being created, and a class that implements this interface. The class and interface are laid out with special WCF attributes, distinguishing them from other types, and specify some details of the behavior during the discovery and service invocation. These types are wrapped into something that works as a server (for example, in a DLL, which IIS is used for), and are supplemented with a configuration file (there are variants), where the details of the service implementation are indicated. Once started, the service can be accessed, for example, by a network address; In the Internet browser, you can see the contracts that the requested service implements.A programmer who wants to access an existing WCF service uses a console utility or a graphical interface built into the development environment to generate C # (or other supported language) types corresponding to the service contracts at the service’s existing address. The file with the obtained types is included in the project of the client application, and after that the programmer uses the same terms that are contained in the service interface, enjoying the benefits of progress (static typing). In addition, the client’s configuration file contains the technical characteristics of the called service (configuration is possible in the code, without a configuration file).WCF supports different modes of transport, encryption and other more subtle technical parameters. Most of them are united by the concept of "binding" (Binding). There are three important WCF service parameters:- the address at which it is available;
- binding;
- contract (interfaces).
All these parameters are set in the service and client configuration files.In our company, WCF (with and without distributed transactions) is widely used in embedded products, however, given the fashion trends, its use in new products is still under question.How to initiate distributed transactions in WCFTo initiate transactions based on WCF System.Transactions
, the programmer needs to place several attributes in the code, make sure that the bindings used support distributed transactions, are written on the client and in the service, transactionFlow="true"
and that a suitable transaction manager is running on all the computers involved. it will be MSDTC).Distributed transaction bindings: NetTcpBinding, NetNamedPipeBinding, WSHttpBinding, WSDualHttpBinding, and WSFederationHttpBinding.The method (operation) of the service interface must be marked with an attribute System.ServiceModel.TransactionFlowAttribute
. Then, with certain attribute parameters and when the TransactionScopeRequired
attribute parameter is set, the System.ServiceModel.OperationBehaviorAttribute
transaction will be distributed between the client and the service. In addition, by default, it is considered that the service votes to commit a transaction, unless an exception was thrown during execution. To change this behavior, you must set the corresponding TransactionAutoComplete
attribute parameter value System.ServiceModel.OperationBehaviorAttribute
.Code of the simplest WCF service supporting distributed transactions. [System.ServiceModel.ServiceContract] public interface IMyService { [System.ServiceModel.OperationContract] [System.ServiceModel.TransactionFlow(System.ServiceModel.TransactionFlowOption.Mandatory)] int DoSomething(string input); } public class MyService : IMyService { [System.ServiceModel.OperationBehavior(TransactionScopeRequired = true)] [System.ServiceModel.TransactionFlow(System.ServiceModel.TransactionFlowOption.Mandatory)] public int DoSomething(string input) { if (input == null) throw new System.ArgumentNullException(nameof(input)); return input.Length; } }
Obviously, it differs from the usual service code only in the use of an attribute System.ServiceModel.TransactionFlow
and in a special attribute setting System.ServiceModel.OperationBehavior
. Configuration example for this service. <system.serviceModel> <services> <service name="WcfWithTransactionsExample.MyService" behaviorConfiguration="serviceBehavior"> <endpoint address="" binding="wsHttpBinding" bindingConfiguration="mainWsBinding" contract="WcfWithTransactionsExample.IMyService"/> <endpoint address="mex" contract="IMetadataExchange" binding="mexHttpBinding"/> </service> </services> <bindings> <wsHttpBinding> <binding name="mainWsBinding" maxReceivedMessageSize="209715200" maxBufferPoolSize="209715200" transactionFlow="true" closeTimeout="00:10:00" openTimeout="00:10:00" receiveTimeout="00:10:00" sendTimeout="00:10:00"> <security mode="None"/> <readerQuotas maxArrayLength="209715200" maxStringContentLength="209715200"/> </binding> </wsHttpBinding> </bindings> </system.serviceModel>
Notice that the WSHttpBinding type binding and the attribute are used transactionFlow="true"
. TL; DR by section
Distributed transactions include multiple resource managers, and all changes must either be committed or rolled back. Some modern DBMS implement distributed transactions, which represent a convenient mechanism for connecting several databases. Software (not implemented in a DBMS) distributed transactions can include different combinations of resource managers on different computers running different operating systems, but they have limitations that need to be considered before relying on them. A modern alternative to distributed transactions are messaging-based solutions. In .NET Core, distributed transactions are not yet supported.WCF is one of the standard and proven tools for creating and accessing services in the .NET world, supporting several modes of transport and encryption. WCF is very close friends with distributed transactions based on System.Transactions
. Setting up distributed transactions for WCF consists in marking the code with several attributes and adding a couple of words in the service and client configuration files. Not all WCF bindings support distributed transactions. In addition, obviously, transactions in WCF have the same limitations as without using WCF. The .NET Core platform so far only allows accessing services on WCF and not creating them.Conclusion-cheat sheet
This publication provides an overview of the fundamentals of .NET software transactions. Some conclusions regarding the trends in software transactions can be found in the sections on the applicability and limitations of the subjects in question, and in the conclusion the main theses of the publication are collected. I suppose that they can be used as a cheat sheet when considering software transactions as one of the options for implementing a technical system or for refreshing relevant information in memory.Transactions (domain, DBMS, software). The requirements of the subject area are sometimes formulated as transactions — operations that, starting in the area of ​​complete data, upon completion (including unsuccessful ones) should also come in the area of ​​complete data (possibly already different). These requirements are usually implemented as system transactions. A classic example of a transaction is the transfer of money between two accounts, consisting of two indivisible operations - withdrawing money from one account and crediting to another. In addition to the well-known transactions implemented by the means of the DBMS, there are also software transactions, for example, in the .NET world. Resource managers are software components that are aware of the existence of such transactions and are able to be included in them, that is, to commit or roll back the changes made.Resource managers receive commit and rollback instructions from the transaction manager that forms the basis of the infrastructureSystem.Transactions
.Durable and non-permanent resource managers. Durable resource managers support data recovery after a system failure. DBMS drivers for .NET usually offer this functionality. Non-permanent resource managers do not support disaster recovery. Software transactional memory — a way to manage objects in RAM — can be viewed as an example of a non-persistent resource manager.Transactions and .NET resource managers. The .NET programmer uses software transactions and creates his own resource managers using types from the namespaceSystem.Transactions
. This infrastructure allows the use of transactions of different nesting and isolation (with known limitations). The use of transactions is not difficult, and it consists in wrapping the code in a block using
with certain characteristics. However, resource managers that are included in the transaction in this way must support the required functionality on their part. Using heterogeneous resource managers in a transaction or using one manager in different ways can automatically turn a transaction into a distributed one.Distributed transactions (DBMS, software).A distributed transaction covers several subsystems, changes in which must be synchronized, that is, they are either fixed together or rolled back. Distributed transactions are implemented in some modern DBMS. Software distributed transactions (these are not the ones that are implemented by the DBMS) impose additional restrictions on the interacting processes and platforms. Distributed transactions are gradually falling out of fashion, yielding to messaging services solutions. To turn a regular transaction into a distributed one, the programmer has little to do: when you include a resource manager with certain characteristics in the transaction, the transaction manager will automatically do everything necessary at run time. Regular software transactions are available in .NET Core and .NET Standard, while distributed transactions are not available.Distributed transactions through WCF. WCF is one of the standard .NET tools for creating and invoking services, including standardized protocols. In other words, the specifically configured WCF services can be accessed from any application, not just .NET or Windows. To create a distributed transaction over WCF, you need to mark up the types that make up the service with additional attributes and make minimal changes to the service and client configuration files. In .NET Core and .NET Standard, you cannot create WCF services, but you can create WCF clients.An example for testing System.Transactions on GitHub