When you send a message inside a transaction, you get a few guarantees that you don't get with a nontransactional message. First, MSMQ provides exactly-once delivery semantics. It takes some extra precautions so that messages that are on their way to a destination queue are not lost or duplicated. MSMQ also ensures that messages inside the same transaction are delivered in the order in which they were sent. MSMQ doesn't use message priorities in transactional queues for this reason. Every message in a transaction queue has a priority level of 0.
Let's look at an example of in-order delivery. Let's say that you send message A and then you send message B to the same queue. Message A will arrive before message B as long as they are part of the same transaction. This means that message A will be placed closer to the head of the queue. However, you should note that other messages from other transactions might be interleaved with yours. MSMQ guarantees the ordering of messages only within a single transaction.
In MSMQ, transactional messages must be sent to transactional queues. You can't change the transactional attribute after a queue has been created. It is therefore important to indicate whether you want a queue to be transactional when you create it. You can use the first parameter of the MSMQQueueInfo component's Create method to indicate whether you want a transactional queue. If you don't pass this parameter, a nontransactional queue will be created by default. It's also easy to indicate that you want a transactional queue when you create one by hand using the MSMQ Explorer.
You can send and receive transactional messages with MSMQ in two ways. You can use MSMQ's internal transactioning mechanism, or you can use external transactions that are coordinated by the Distributed Transaction Coordinator (DTC) with the two-phase commit protocol (described in Chapter 10). Each technique offers distinct advantages.
MSMQ provides its own internal transactioning mechanism, which provides the best performance. However, when you use internal transactions, MSMQ can't coordinate the transaction with any other type of resource manager, such as a SQL Server database. Internal transactions do not use the DTC or the two-phase commit protocol. Instead, MSMQ uses a more efficient protocol that is especially tuned for transactional messaging. Consequently, internal transactions are much faster than externally coordinated transactions.
Because MSMQ supports the OLE Transactions protocol, you can use it along with another resource manager when you create DTC-based transactions. Your connection to a transactional queue can be enlisted with the DTC. External transactions are also known as coordinated transactions because they are managed by the DTC. While external transactions are slower than internal transactions, they let you define transactions that include message passing along with operations to other resource managers. For example, you can write a transaction that receives a request message, modifies a SQL Server database, and sends a response message. Because the three operations are part of a single transaction, the DTC enforces the ACID rules (described in Chapter 10).
MSMQ lets you explicitly begin and control an external DTC-based transaction. However, it's hard for you to do much in Visual Basic with the DTC without using MTS because you have to enlist any other resource manager connection yourself. Fortunately, you can send and receive messages from inside an MTS transaction and let MTS deal with enlisting the connection to a DBMS. This makes it easy to mix message passing and DBMS access inside a single transaction. The MTS run time will enlist all the resource managers (including MSMQ) for you behind the scenes.
MSMQ provides a shortcut when you want to send only a single transacted message. To send a single message in its own transaction, pass the constant MQ_SINGLE_MESSAGE when you call Send. Here's what a call to Send looks like:
msg.Send q, MQ_SINGLE_MESSAGE
You'll often want to send single messages inside a transaction because you'll want to guarantee exactly-once delivery. If you don't send the message inside a transaction, it might be lost or duplicated.
If you want to send multiple messages inside an internal transaction, you must create a new MSMQTransactionDispenser object. You can then invoke its BeginTransaction method to start a new transaction. A call to BeginTransaction returns an MSMQTransaction object. You can pass a reference to this MSMQTransaction object when you call a message writing operation such as Send, Receive, or ReceiveCurrent. The code below is an example of sending two messages to one transaction queue while performing a receive operation on another transactional queue.
Dim td As MSMQTransactionDispenser Set td = New MSMQTransactionDispenser Dim tx As MSMQTransaction Set tx = td.BeginTransaction() ' Send and receive message from transactional queues. msg1.Send q1, tx msg2.Send q1, tx Set msg3 = q2.Receive(Transaction:=tx, ReceiveTimeOut:=0) ' Commit the transaction. tx.Commit
The messages are not sent to the queue until the transaction has been committed. In this example, you can be sure that msg1 will be placed in the queue ahead of msg2. When you receive a message inside a transaction, it is removed from the queue immediately. Another application won't see the message in the queue, even though the transaction hasn't been committed. However, if the transaction is aborted, the message is returned to the queue. This behavior is the same for transactional queues whether you are using internal transactions or external transactions.
You must follow certain rules when you program against transactional queues. If you violate one of the rules, you will experience a run-time error.
Another thing to keep in mind is that MSMQ transactions run at a lower isolation level than those in SQL Server. As you remember from Chapter 10, SQL Server will always conduct the operations inside an MTS transaction with an isolation level of Serializable. MSMQ doesn't support the Serializable isolation level because of the way it manages locks during a transaction.
MSMQ provides an isolation level of Read Committed between a receiving transaction and a sending transaction. The receiver can see messages from the committed send transaction even if the send transaction started after the receive transaction. If MSMQ were to run transactions with an isolation level of Serializable, it would have to put a lock on the queue and block send operations until the receiving transaction was complete. A locking scheme such as this would pose an unacceptable concurrency restraint in a queuing system.
With respect to two receiving transactions, however, the isolation level is Read Uncommitted. Let's look at an example to demonstrate this point. Assume there is a single message in a transactional queue. If transaction A receives the message from the queue, transaction B will see the queue as empty. If transaction A later aborts, the message will be written back to the queue. The queue was really never empty. However, transaction B saw the queue as empty because it read the queue in an uncommitted state. You shouldn't see the inability to run serializable transactions as a flaw in MSMQ. It's really just an inherent problem with transactional queuing. Isolation is sacrificed to maintain higher levels of concurrency. This means you can't make as many assumptions as you can when working with a DBMS that runs its operations with an isolation level of Serializable.
MSMQ includes an ActiveX component for creating DTC-based transactions named the MSMQCoordinatedTransactionDispenser. This component's interface is identical to the MSMQTransactionDispenser component that you saw earlier in this chapter. Both components expose a single method, BeginTransaction, which returns an MSMQTransaction object. In fact, in most of your code you can use these two components interchangeably.
The difference between the two components is that the coordinated dispenser works with the DTC, while the internal dispenser doesn't. When you commit an external transaction, the DTC executes the two-phase protocol against MSMQ, which acts as a resource manager. The coordinated dispenser also lets you enlist a resource manager other than MSMQ into a transaction. There's currently no straightforward way, however, to enlist another resource manager such as SQL Server using the MSMQTransaction object. This means that most Visual Basic programmers can't benefit from using the coordinated dispenser directly.
Fortunately, you can get the same benefits of external transactions by sending and receiving messages from within an MTS transaction. The MTS run time can enlist MSMQ as well as other resource managers such as SQL Server and Oracle into a single transaction. Here's an example of sending a message inside a transactional MTS component:
Dim ObjCtx As ObjectContext Set ObjCtx = GetObjectContext() ' First, write a few records to a SQL Server database. ' Second, send a message to MSMQ. Dim qi As MSMQQueueInfo Set qi = New MSMQQueueInfo qi.PathName = ".\MyResponseQueue" Dim q As MSMQQueue Set q = qi.Open(MQ_SEND_ACCESS, MQ_DENY_NONE) Dim msg As MSMQMessage Set msg = New MSMQMessage msg.Body = "The database has been updated" msg.Send q, MQ_MTS_TRANSACTION q.Close ObjCtx.SetComplete ' Or call SetAbort.
You can pass the constant MQ_MTS_TRANSACTION in a call to Send, Receive, or ReceiveCurrent to indicate that you want your writing operation to be part of the current MTS transaction. If you don't pass a transaction parameter, this value is also the default. Note that passing MQ_MTS_TRANSACTION results in a transacted operation if you make the call from within an MTS transaction.
Passing MQ_MTS_TRANSACTION results in a Wrong Transaction Usage error if your program attempts to send a message inside MTS outside the scope of a transaction. Passing MQ_MTS_TRANSACTION will result in a nontransacted receive if your program calls Receive or ReceiveCurrent inside MTS outside the scope of a transaction.
As in the case of an internal transaction, messages inside an MTS transaction are not actually sent until the transaction has been committed. When you receive messages inside a transaction, they are removed from the queue right away. If you call SetAbort to roll back a transaction, your messages are written back to the queue. Remember, there are no guarantees that the message will be placed back in the same positions. The previous description of MSMQ isolation levels applies to external transactions as well as internal transactions.