j2ee‎ > ‎

Java theory and practice: Understanding JTS -- Balancing safety and performance

posted Jun 20, 2010, 7:43 PM by Kuwon Kang

Java theory and practice: Understanding JTS -- Balancing safety and performance

Guidelines for transaction demarcation and isolation
Brian Goetz (brian@quiotix.com), Principal Consultant, Quiotix Corp
Summary: In Parts 1 and 2 of his series on JTS, Brian covered the basics of what transactions are and how J2EE containers make transaction services transparent to EJB components. While being able to specify a component's transactional semantics declaratively instead of programmatically offers a great deal of flexibility in configuring enterprise applications, making poor decisions at application assembly time can impair the performance and stability of your applications. In this final installment, Brian looks at the facilities that J2EE offers for managing transaction demarcation and isolation and some guidelines for using them effectively.

View more content in this series

Date: 01 May 2002 Level: Intermediate Activity: 2095 views Comments: 0 (Add comments)

1 star2 stars3 stars4 stars5 stars Average rating (based on 10 votes)
In Part 1 ("An introduction to transactions") and Part 2 ("The magic beind the scenes") of this series, we defined what a transaction is, enumerated the basic properties of transactions, and explored how Java Transaction Service and J2EE containers work together to provide transparent support for transactions to J2EE components. In this article, we'll take on the topic of transaction demarcation and isolation. The responsibility for defining transaction demarcation and isolation attributes for EJB components lies with the application assembler. Setting these improperly can have serious consequences for the performance, scalability, or fault-tolerance of the application. Unfortunately, there are no hard-and-fast rules for setting these attributes properly, but there are some guidelines that can help us find a balance between concurrency hazards and performance hazards. As we discussed in Part 1, transactions are primarily an exception-handling mechanism. Transactions serve a similar purpose in programs that legal contracts do in everyday business: they help us recover if something goes wrong. But because most of the time nothing actually goes wrong, we'd like to be able to minimize their cost and intrusion the rest of the time. How we use transactions in our applications can have a big effect on application performance and scalability. Transaction demarcation J2EE containers provide two mechanisms for defining where transactions begin and end: bean-managed transactions and container-managed transactions. In bean-managed transactions, you begin and end a transaction explicitly in the bean methods with UserTransaction.begin() and UserTransaction.commit(). Container-managed transactions, on the other hand, offer a lot more flexibility. By defining transactional attributes for each EJB method in the assembly descriptor, you can specify what the transactional requirements are for each method and let the container determine when to begin and end a transaction. In either case, the basic guidelines for structuring transactions are the same. Get in, get out The first rule of transaction demarcation is "Keep it short." Transactions provide concurrency control; this generally means that the resource manager will acquire locks on your behalf on data items you access during the course of a transaction, and it must hold them until the transaction ends. (Recall from the ACID properties discussed in Part 1 of this series that the "I" in "ACID" stands for "Isolation." That is, the effects of one transaction do not affect other transactions that are executing concurrently.) While you are holding locks, any other transaction that needs to access the data items you have locked will have to wait until you release the locks. If your transaction is very long, all those other transactions will be blocked, and your application throughput will plummet.
Rule 1: Keep transactions as short as possible.
By keeping transactions short, you minimize the time you are in the way of other transactions and thereby enhance your application's scalability. The best way to keep transactions as short as possible, of course, is not to do anything that is unnecessarily time consuming in the middle of a transaction, and in particular don't wait for user input in the middle of a transaction. It may be tempting to begin a transaction, retrieve some data from a database, display the data, and then ask the user to make a choice while still in the transaction. Don't do this! Even if the user is paying attention, it will still take seconds to respond -- a long time to be holding locks in the database. And what if the user decided to step away from the computer, perhaps for lunch or even to go home for the day? The application will simply grind to a halt. Doing I/O during a transaction is a recipe for disaster.
Rule 2: Don't wait for user input during a transaction.
Group related operations together Because each transaction has non-trivial overhead, you might think it's best to perform as many operations in a single transaction as possible to minimize the per-operation overhead. However, Rule 1 tells us that long transactions are bad for scalability. So how do we achieve a balance between minimizing per-operation overhead and scalability? Taking Rule 1 to its logical extreme -- one operation per transaction -- would not only introduce additional overhead, but could also compromise the consistency of the application state. Transactional resource managers are supposed to maintain the consistency of the application state (recall from Part 1 that the "C" in "ACID" stands for "Consistency"), but they rely on the application to define what consistency means. In fact, the definition of consistency we used when describing transactions is somewhat circular: consistency means whatever the application says it is. The application organizes groups of changes to the application state into transactions, and the resulting application state is by definition consistent. The resource manager then ensures that if it has to recover from a failure, the application state is restored to the most recent consistent state. In Part 1, we gave an example of transferring funds from one account to another in a banking application. Listing 1 shows a possible implementation of this in SQL, which contains five SQL operations (a select, two updates, and two inserts):
SELECT accountBalance INTO aBalance 
    FROM Accounts WHERE accountId=aId;
IF (aBalance >= transferAmount) THEN 
    UPDATE Accounts 
        SET accountBalance = accountBalance - transferAmount
        WHERE accountId = aId;
    UPDATE Accounts 
        SET accountBalance = accountBalance + transferAmount
        WHERE accountId = bId;
    INSERT INTO AccountJournal (accountId, amount)
        VALUES (aId, -transferAmount);
    INSERT INTO AccountJournal (accountId, amount)
        VALUES (bId, transferAmount);
ELSE
    FAIL "Insufficient funds in account";
END IF
What would happen if we executed this operation as five separate transactions? Not only would it be slower (because of the transaction overhead), but we would lose consistency as well. What would happen, for instance, if someone withdrew money from account A as part of a separate transaction between the execution of the first SELECT (checking the balance) and the subsequent debit UPDATE? This would violate a business rule that is supposed to be enforced by this code (that account balances should be non-negative). What if the system fails between the first UPDATE and the second? Now, when the system recovers, the money will have left account A but will not have been credited to account B, and there will be no record of why. This is not going to make the owners of either account very happy. The five SQL operations in Listing 1 are part of a single related operation: transferring funds from one account to another. Therefore, we would want either all of them to execute or none of them to execute, suggesting that they should all be executed in a single transaction.
Rule 3: Group related operations into a single transaction.
The ideal balance Rule 1 said transactions should be as short as possible. The example from Listing 1 shows that sometimes we have to group operations together into a transaction to maintain consistency. Of course, it depends on the application to determine what constitutes "related operations." We can combine Rules 1 and 3 to give a general guideline for describing the scope of a transaction, which we will state as Rule 4:
Rule 4: Group related operations into a single transaction, but put unrelated operations into separate transactions.

Back to top

Container-managed transactions When we use container-managed transactions, instead of explicitly stating where transactions start and end, we define transactional requirements for each EJB method. The transaction mode is defined in the trans-attribute element of thecontainer-transaction section of the bean's assembly-descriptor. (An example assembly-descriptor is shown in Listing 2.) The method's transaction mode, along with the state of whether the calling method is already enlisted in a transaction, determines which of several actions the container takes when an EJB method is called:
  • Enlist the method in an existing transaction.
  • Create a new transaction and enlist the method in it.
  • Do not enlist the method in any transaction.
  • Throw an exception.
<assembly-descriptor>
  ...
  <container-transaction>
    <method>
      <ejb-name>MyBean</ejb-name>
      <method-name>*</method-name>
    </method>
    <trans-attribute>Required</trans-attribute>
  </container-transaction>
  <container-transaction>
    <method>
      <ejb-name>MyBean</ejb-name>
      <method-name>logError</method-name>
    </method>
    <trans-attribute>RequiresNew</trans-attribute>
  </container-transaction>
  ...
</assembly-descriptor>
The J2EE specification defines six transaction modes: RequiredRequiresNewMandatorySupportsNotSupported, and Never. Table 1 summarizes the behavior of each mode -- both when called in an existing transaction and when called while not in a transaction -- and describes which types of EJB components support each mode. (Some containers may permit you greater flexibility in choosing transaction modes, but such use would be relying on a container-specific feature and hence would not be portable across containers.) Table 1. Transaction modes
Transaction mode Bean types Action when called in transaction T Action when called outside of a transaction
Required Session, Entity, Message-driven Enlist in T New transaction
RequiresNew Session, Entity New transaction New transaction
Supports Session, Message-driven Enlist in T Run without transaction
Mandatory Session, Entity Enlist in T Error
NotSupported Session, Message-driven Run without transaction Run without transaction
Never Session, Message-driven Error Run without transaction
In an application that uses only container-managed transactions, the only way a transaction will be started is if a component calls an EJB method whose transaction mode is Required or RequiresNew. When the container creates a transaction as a result of calling a transactional method, that transaction will be closed when the method completes. If the method returns normally, the container will commit the transaction (unless the application has asked for the transaction to be rolled back). If the method exits by throwing an exception, the container will roll back the transaction and propagate the exception. If a method is called in an existing transaction T and the transaction mode specifies that the method should be run without a transaction or run in a new transaction, transaction T is suspended until the method completes, and then the previous transaction T is resumed. Choosing a transaction mode So which mode should we choose for our bean methods? For session and message-driven beans, you will usually want to useRequired to ensure that every call will be executed as part of a transaction, but will still allow the method to be a component of a larger transaction. Exercise care with RequiresNew; it should only be used when you are sure that the actions of your method should be committed separately from the actions of the method that called you. RequiresNew is typically used only with objects that have little or no relation to other objects in the system, such as logging objects. (Using RequiresNew with a logging object makes sense because you would want the log message to be committed regardless of whether the enclosing transaction commits.) Using RequiresNew in an inappropriate manner can result in a situation similar to the one described above, where the code in Listing 1 was executed in five separate transactions instead of one, which can leave your application in an inconsistent state. For CMP (container-managed persistence) entity beans, you will usually want to use RequiredMandatory is also a reasonable option, especially for initial development; this will alert you to cases where your entity bean methods are being called outside of a transaction, which may indicate a deployment error. You almost never want to use RequiresNew with CMP entity beans.NotSupported and Never are intended for nontransactional resources, such as adapters for foreign nontransactional systems or for transactional systems that cannot be enlisted in a Java Transaction API (JTA) transaction. When EJB applications are properly designed, applying the above guidelines for transaction modes tends to naturally yield the transaction demarcation suggested by Rule 4. The reason is that J2EE architecture encourages decomposition of the application into the smallest convenient processing chunks, and each chunk is processed as an individual request (whether in the form of an HTTP request or as the result of a message being queued to a JMS queue).

Back to top

Isolation revisited In Part 1, we defined isolation to mean that the effects of one transaction are not visible to other transactions executing concurrently; from the perspective of a transaction, it appears that transactions execute sequentially rather than in parallel. While transactional resource managers can often process many transactions simultaneously while providing the illusion of isolation, sometimes isolation constraints actually require that beginning a new transaction be deferred until an existing transaction completes. Since completing a transaction involves at least one synchronous disk I/O (to write to the transaction log), this could limit the number of transactions per second to something close to the number of disk writes per second, which would not be good for scalability. In practice, it is common to relax the isolation requirements substantially to allow more transactions to execute concurrently and enable improved system response and greater scalability. Nearly all databases support four standard isolation levels: Read Uncommitted, Read Committed, Repeatable Read, and Serializable. Unfortunately, managing isolation for container-managed transactions is currently outside the scope of the J2EE specification. However, many J2EE containers, such as IBM WebSphere and BEA WebLogic, provide container-specific extensions that allow you to set transaction isolation levels on a per-method basis in the same manner as transaction modes are set in the assembly-descriptor. For bean-managed transactions, you can set isolation levels via the JDBC or other resource manager connection. To illustrate the differences between the isolation levels, let's first categorize several concurrency hazards -- cases where one transaction might interfere with another in the absence of suitable isolation. All of the following hazards have to do with the results of one transaction becoming visible to a second transaction after the second transaction has already started:
  • Dirty Read: Occurs when the intermediate (uncommitted) results of one transaction are made visible to another transaction.
  • Unrepeatable Read: Occurs when one transaction reads a data item and subsequently rereads the same item and sees a different value.
  • Phantom Read: Occurs when one transaction performs a query that returns multiple rows, and later executes the same query again and sees additional rows that were not present the first time the query was executed.
The four standard isolation levels are related to these three isolation hazards, as shown in Table 2. The lowest isolation level, Read Uncommitted, provides no protection against changes made by other transactions, but is the fastest because it doesn't require contention for read locks. The highest isolation level, Serializable, is equivalent to the definition of isolation given above; each transaction appears to be fully isolated from the effects of other transactions. Table 2. Transaction isolation levels
Isolation Level Dirty read Unrepeatable read Phantom read
Read Uncommitted Yes Yes Yes
Read Committed No Yes Yes
Repeatable Read No No Yes
Serializable No No No
For most databases, the default isolation level is Read Committed, a good default choice because it prevents transactions from seeing an inconsistent view of the application data at any given point in the transaction. Read Committed is a good isolation level to use for most typical short transactions, such as when fetching data for reports or to be displayed to a user (perhaps as a result of a Web request), and for inserting new data into the database. The higher isolation levels, Repeatable Read and Serializable, are suitable when you require a greater degree of consistency throughout the transaction, such as in the example of Listing 1, where you would want the account balance to stay the same from the time you check to ensure there are sufficient funds to the time you actually debit the account; this requires an isolation level of at least Repeatable Read. In cases where data consistency is absolutely essential, such as auditing an accounting database to make sure the sum of all debits and credits to an account equals its current balance, you would also need protection against new rows being created. This would be a case where you would need to use Serializable. The lowest isolation level, Read Uncommitted, is rarely used. It is suitable for when you need only to obtain an approximate value, and the query would otherwise impose undesired performance overhead. A typical use for Read Uncommitted is when you want to estimate a rapidly varying quantity like the number of orders or the total dollar volume of orders placed today. Because there is a substantial trade-off between isolation and scalability, you should exercise care in selecting an isolation level for your transactions. Selecting too low a level can be hazardous to your data. Selecting too high a level might be bad for performance, although at light loads it might not be. In general, data consistency problems are more serious than performance problems. If in doubt, you should err on the side of caution and choose a higher isolation level. And that brings us to Rule 5:
Rule 5: Use the lowest isolation level that keeps your data safe, but if in doubt, use Serializable.
Even if you are planning to initially err on the side of caution and hope that the resulting performance is acceptable (the performance management technique called "denial and prayer" -- probably the most commonly employed performance strategy, though most developers will not admit it), it pays to think about isolation requirements as you are developing your components. You should strive to write transactions that are tolerant of lower isolation levels where practical, so as not to paint yourself into a corner later on if performance becomes an issue. Because you need to know what a method is doing and what consistency assumptions are buried within it to correctly set the isolation level, it is also a good idea to carefully document concurrency requirements and assumptions during development, so as to assist in making correct decisions at application assembly time.

Back to top

Conclusion Many of the guidelines offered in this article may appear somewhat contradictory, because issues such as transaction demarcation and isolation are inherently trade-offs. We're trying to balance safety (if we didn't care about safety, we wouldn't bother with transactions at all) against the performance overhead of the tools we're using to provide that margin of safety. The correct balance is going to depend on a host of factors, including the cost or damage associated with system failure or downtime and your organizational risk tolerance. Resources About the author
Brian Goetz is a software consultant and has been a professional software developer for the past 15 years. He is a Principal Consultant at Quiotix, a software development and consulting firm located in Los Altos, California. See Brian's published and upcoming articles in popular industry publications.
Comments