ACID Transactions
Declarative Transaction Management
Isolation and Database Locking
Non-Transactional Beans
Explicit Transaction Management
EJB 1.1: Exceptions and Transactions
EJB 1.0: Exceptions and Transactions
Transactional Stateful Session Beans
To understand how transactions work, we will revisit the TravelAgent bean, a stateful session bean that encapsulates the process of making a cruise reservation for a customer. Here is the TravelAgent's bookPassage() method:
public Ticket bookPassage(CreditCard card, double price) throws IncompleteConversationalState { // EJB 1.0: also throws RemoteException if (customer == null || cruise == null || cabin == null){ throw new IncompleteConversationalState(); } try { ReservationHome resHome = (ReservationHome) getHome("ReservationHome",ReservationHome.class); Reservation reservation = resHome.create(customer, cruise, cabin, price); ProcessPaymentHome ppHome = (ProcessPaymentHome) getHome("ProcessPaymentHome",ProcessPaymentHome.class); ProcessPayment process = ppHome.create(); process.byCredit(customer, card, price); Ticket ticket = new Ticket(customer,cruise,cabin,price); return ticket; } catch(Exception e) { // EJB 1.0: throw new RemoteException("",e); throw new EJBException(e); } }
The TravelAgent bean is a fairly simple session bean, and its use of other beans is a typical example of business object design and workflow. Unfortunately, good business object design is not enough to make these beans useful in an industrial-strength application. The problem is not with the definition of the beans or the workflow; the problem is that a good design doesn't, in and of itself, guarantee that the TravelAgent's bookPassage() method represents a good transaction. To understand why, we will take a closer look at what a transaction means and what criteria a transaction must meet to be considered reliable.
In business, a transaction usually involves an exchange between two parties. When you purchase an ice cream cone, you exchange money for food; when you work for a company, you exchange skill and time for money (which you use to buy more ice cream). When you are involved in these exchanges, you monitor the outcome to ensure that you don't get "ripped off." If you give the ice cream vendor a $20 bill, you don't want him to drive off without giving you your change; you want to make sure that your paycheck reflects all the hours that you worked. By monitoring these commercial exchanges, you are attempting to ensure the reliability of the transactions; you are making sure that the transaction meets everyone's expectations.
In business software, a transaction embodies the concept of a commercial exchange. A business system transaction (transaction for short) is the execution of a unit-of-workthat accesses one or more shared resources, usually databases. A unit-of-work is a set of activities that relate to each other and must be completed together. The reservation process is a unit-of-work made up of several activities: recording a reservation, debiting a credit card, and generating a ticket together make up a unit-of-work.
Transactions are part of many different types of systems. In each transaction, the objective is the same: to execute a unit-of-work that results in a reliable exchange. Here are some examples of other types of business systems that employ transactions:
The ATM (automatic teller machine) you use to deposit, withdraw, and transfer funds, executes these units-of-work as transactions. In an ATM withdrawal, for example, the ATM checks to make sure you don't overdraw and then debits your account and spits out some money.
You've probably purchased many of your Java books from an online bookseller--maybe even this book. This type of purchase is also a unit-of-work that takes place as a transaction. In an online book purchase, you submit your credit card number, it is validated, and then a charge is made for price of the book, and an order to ship you the book is sent to the bookseller's warehouse.
In a medical system, important data--some of it critical--is recorded about patients every day, including information about clinical visits, medical procedures, prescriptions, and drug allergies. The doctor prescribes the drug, then the system checks for allergies, contraindications, and appropriate dosages. If all tests pass, then the drug can be administered. The tasks just described make up a unit-of-work in a medical system. A unit-of-work in a medical system may not be financial, but it's just as important. A failure to identify a drug allergy in a patient could be fatal.
As you can see, transactions are often complex and usually involve the manipulation of a lot of data. Mistakes in data can cost money, or even a life. Transactions must therefore preserve data integrity, which means that the transaction must work perfectly every time or not be executed at all. This is a pretty tall order, especially for complex systems. As difficult as this requirement is, however, when it comes to commerce there is no room for error. Units-of-work that involve money, or anything of value, always require the utmost reliability because errors impact the revenues and the well-being of the parties involved.
To give you an idea of the accuracy required by transactions, think about what would happen if a transactional system suffered from seemingly infrequent errors. ATMs provide customers with convenient access to their bank accounts and represent a significant percentage of the total transactions in personal banking. The number of transactions handled by ATMs are simple but numerous, providing us with a great example of why transactions must be error proof. Let's say that a bank has 100 ATMs in a metropolitan area, and each ATM processes 300 transactions (deposits, withdrawals, or transfers) a day for a total of 30,000 transactions per day. If each transaction, on average, involves the deposit, withdrawal, or transfer of about $100, about three million dollars would move through the ATM system per day. In the course of a year, that's a little over a billion dollars:
(365 days) × (100 ATMs) × (300 transactions) × ($100.00) = $1,095,000,000.00
How well do the ATMs have to perform in order for them to be considered reliable? For the sake of argument, let's say that ATMs execute transactions correctly 99.99% of the time. This seems to be more than adequate: after all, only one out of every ten thousand transactions executes incorrectly. But over the course of a year, if you do the math, that could result in over $100,000 in errors!
$1,095,000,000.00 × .01% = $109,500.00
Obviously, this is an oversimplification of the problem, but it illustrates that even a small percentage of errors is unacceptable in high-volume or mission-critical systems. For this reason, experts in the field of transaction services have identified four characteristics of a transaction that must be followed in order to say that a system is safe. Transactions must be atomic, consistent, isolated, and durable (ACID)--the four horsemen of transaction services. Here's what each term means:
To be atomic, a transaction must execute completely or not at all. This means that every task within a unit-of-work must execute without error. If any of the tasks fails, the entire unit-of-work or transaction is aborted, meaning that changes to the data are undone. If all the tasks execute successfully, the transaction is committed, which means that the changes to the data are made permanent or durable.
Consistency is a transactional characteristic that must be enforced by both the transactional system and the application developer. Consistency refers to the integrity of the underlying data store. The transactional system fulfills its obligation in consistency by ensuring that a transaction is atomic, isolated, and durable. The application developer must ensure that the database has appropriate constraints (primary keys, referential integrity, and so forth) and that the unit-of-work, the business logic, doesn't result in inconsistent data (data that is not in harmony with the real world it represents). In an account transfer, for example, a debit to one account must equal the credit to the other account.
A transaction must be allowed to execute without interference from other processes or transactions. In other words, the data that a transaction accesses cannot be affected by any other part of the system until the transaction or unit-of-work is completed.
Durability means that all the data changes made during the course of a transaction must be written to some type of physical storage before the transaction is successfully completed. This ensures that the changes are not lost if the system crashes.
To get a better idea of what these principles mean, we will examine the TravelAgent bean in terms of the four ACID properties.
Our first measure of the TravelAgent bean's reliability is its atomicity: does it ensure that the transaction executes completely or not at all? What we are really concerned with are the critical tasks that change or create information. In the bookPassage() method, a Reservation bean is created, the ProcessPayment bean debits a credit card, and a Ticket object is created. All of these tasks must be successful for the entire transaction to be successful.
To understand the importance of the atomic characteristic, you have to imagine what would happen if even one of the subtasks failed to execute. If, for example, the creation of a Reservation failed but all other tasks succeeded, your customer would probably end up getting bumped from the cruise or sharing the cabin with a stranger. As far as the travel agent is concerned, the bookPassage() method executed successfully because a Ticket was generated. If a ticket is generated without the creation of a reservation, the state of the business system becomes inconsistent with reality because the customer paid for a ticket but the reservation was not recorded. Likewise, if the ProcessPayment bean fails to charge the customer's credit card, the customer gets a free cruise. He may be happy, but management isn't. Finally, if the Ticket is never created, the customer would have no record of the transaction and probably wouldn't be allowed onto the ship.
So the only way bookPassage() can be completed is if all the critical tasks execute successfully. If something goes wrong, the entire process must be aborted. Aborting a transaction requires more than simply not finishing the tasks; in addition, all the tasks that did execute within the transaction must be undone. If, for example, the creation of the Reservation bean and ProcessPayment.byCredit() method succeeded but the creation of the Ticket failed, then the Reservation record and payment records must not be added to the database.
In order for a transaction to be consistent, the state of the business system must make sense after the transaction has completed. In other words, the state of the business system must be consistent with the reality of the business. This requires that the transaction enforce the atomic, isolated, and durable characteristics of the transaction, and it also requires diligent enforcement of integrity constraints by the application developer. If, for example, the application developer fails to include the credit card charge operation in the bookPassage() method, the customer would be issued a ticket but would never be charged. The data would be inconsistent with the expectation of the business--a customer should be charged for passage. In addition, the database must be set up to enforce integrity constraints. For example, it should not be possible for a record to be added to the RESERVATION table unless the CABIN_ID, CRUISE_ID, and CUSTOMER_ID foreign keys map to corresponding records in the CABIN, CRUISE, and CUSTOMER tables, respectively. If a CUSTOMER_ID is used that doesn't map to a CUSTOMER record, referential integrity should cause the database to throw an error message.
If you are familiar with the concept of thread synchronization in Java or row-locking schemes in relational databases, isolation will be a familiar concept. To be isolated, a transaction must protect the data that it is accessing from other transactions. This is necessary to prevent other transactions from interacting with data that is in transition. In the TravelAgent bean, the transaction is isolated to prevent other transactions from modifying the beans that are being updated. Imagine the problems that would arise if separate transactions were allowed to change any entity bean at any time--transactions would walk all over each other. You could easily have several customers book the same cabin because their travel agents happened to make their reservations at the same time.
The isolation of data accessed by beans doesn't mean that the entire application shuts down during a transaction. Only those entity beans and data directly affected by the transaction are isolated. In the TravelAgent bean, for example, the transaction isolates only the Reservation bean created. There can be many Reservation beans in existence; there's no reason these other beans can't be accessed by other transactions.
To be durable, the funds transfer must write all changes and new data to a permanent data store before it can be considered successful. While this may seem like a no-brainer, often it isn't what happens in real life. In the name of efficiency, changes are often maintained in memory for long periods of time before being saved on a disk drive. The idea is to reduce disk accesses--which slow systems down--and only periodically write the cumulative effect of data changes. While this approach is great for performance, it is also dangerous because data can be lost when the system goes down and memory is wiped out. Durability requires the system to save all updates made within a transaction as the transaction successfully completes, thus protecting the integrity of the data.
In the TravelAgent bean, this means that the new RESERVATION and PAYMENT records inserted are made persistent before the transaction can complete successfully. Only when the data is made durable are those specific records accessible through their respective beans from other transactions. Hence, durability also plays a role in isolation. A transaction isn't finished until the data is successfully recorded.
Ensuring that transactions adhere to the ACID principles requires careful design. The system has to monitor the progress of a transaction to ensure that it does all its work, that the data is changed correctly, that transactions don't interfere with each other, and that the changes can survive a system crash. Engineering all this functionality into a system is a lot of work, and not something you would want to reinvent for every business system you worked on. Fortunately, EJB is specifically designed to support transactions automatically, making the development of transactional systems easier. The rest of this chapter examines how EJB supports transactions implicitly (through declarative transaction attributes) and explicitly (through the Java Transaction API).
Copyright © 2001 O'Reilly & Associates. All rights reserved.