DekGenius.com
[ Team LiB ] Previous Section Next Section

10.2 General Concurrency Patterns

System transactions can solve half of our concurrency problems, but they only really help for activities that can be run all at once. In other words, a system transaction works well when we can run four SQL statements one after the other in a single call to a DAO or during the duration of a single HTTP request. But once we start working with longer-lived transactions, things get messy. While a system transaction is running, the ACID properties limit the access that the outside world has to the data the transaction is working with. If a transaction is over in 500 milliseconds, this limitation isn't always a problem. If a transaction lasts 10 minutes, though—particularly if it prevents any other processes from even reading the underlying tables—we've got a real problem. And what happens if the user gets up and walks away?

At first, none of this may seem like a big deal: how often do we spread transactions across multiple pages? The answer is: surprisingly often. Take that most common of use cases, the Edit Data screen. Even if all the data to be changed fits on a single page, how are you going to deal with the conflicts that occur if two users are editing simultaneously? If two users start editing a single record, the first to save her changes might have all her work transparently destroyed the moment the second user clicks the Save button.

If you can confine all of your concurrency worries to a single request, you're lucky. Skip ahead to the next chapter, which talks about enterprise messaging and shows you how to do some pretty cool stuff. But if you're like the rest of us, read on.

This section proposes two patterns for controlling concurrency across long-running transactions, particularly those that span multiple user interactions with the system. In the last section of the chapter, we'll look at a few patterns that can be used to implement either approach.

All of these patterns help solve the problem of offline concurrency: maintaining a transaction even when the user isn't actually doing anything. In a web application, the user is offline between each request to the web server, so this issue often comes up.

10.2.1 Locking Resources

Before we proceed, we need to go into a more detail about the mechanics of protecting data. The primary mechanism for preventing and resolving resource conflicts is called a lock. The term should be familiar to anyone who has worked with file I/O in multiuser or multithreaded systems, or to anyone who has used a document repository or version control system. A thread can place a lock on a resource, signaling to the rest of the system that it needs to perform some action and the rest of the system must wait for access until the lock is released. Databases, EJB servers, and other transaction managers use locks to enforce the ACID properties on transactions. For example, a transaction might lock a particular row within a database table.

There are three main kinds of locks: read-only locks, write locks, and exclusive locks. A read-only lock indicates that a resource is being accessed but not modified, and that the resource cannot be modified during the read process. An object can support multiple read-only locks, and will become writeable when the last lock is released. A write lock claims the privilege of writing to an object: only the lock holder can modify the resource until the lock is released. Any other threads attempting to change the resource have to wait. An exclusive lock (also known as a read-write lock) grabs exclusive access to a resource, preventing other threads, users, or resources from either reading or modifying it.

If you're using locks at all, you'll definitely use a write lock. This type of lock prevents the typical thread conflict situation, in which one thread reads from an object, another thread writes to it, the first thread writes to the object, and the second thread reads from it. In this case, the second thread doesn't experience the behavior it expects.

Read-only locks are only needed when a consistent view of the data is critical. Since a read-only lock can't be obtained when a write lock exists, an application can use them to prevent dirty reads, where an object is seen in mid-transaction state. Read-only locks are the primary mechanism for supporting atomicity in transactions.

The more oppressive the lock, the greater the performance penalty. Exclusive locks, which prevent any access to an object, are the most costly. The cost comes both from the overhead of managing the lock itself and the delays imposed upon the rest of the application when accessing an object.

10.2.2 Optimistic Concurrency Pattern

Many systems have large numbers of objects subject to transaction control, but relatively few transactions affecting the same objects at the same time. These applications require a high-performance solution that will maintain transactional integrity without penalizing performance. The Optimistic Concurrency pattern provides a solution.

An optimistic locking scheme assumes that the chance of collisions is low. The system runs under the basic assumption that if something does go wrong, it's OK for the end user to have to do a little more work to resolve the problem, such as rekeying a little data or coming back later and trying again. This approach doesn't excuse you from identifying and preventing the conflict: it just means that you don't have to do it preemptively. If one out of every thousand transactions has to be rekeyed, so be it—just make sure that you let people know when they have to do it.

The simplest strategy for optimistic concurrency is to implement a versioning scheme. Each entity under concurrency control is given a version identifier, which is changed every time the data is altered. When modifying an object, you note the version number, make your changes, and commit those changes if someone else hasn't changed the version number of the underlying object since you started making your changes. In an object-only environment, the process might look like this:

  1. Clone the object.

  2. Make changes to the clone.

  3. Check the version number of the original object.

  4. If the versions are the same, replace the original with the clone and update the version. Otherwise, give the user an error and have him repeat steps 1 through 3.

If you start this process while someone else is already on step 2, and he finishes before you get to steps 3 and 4, you'll get an error on step 4. (We'll look at the Version Number pattern in more detail later in this chapter.)

10.2.3 Pessimistic Concurrency Pattern

If an application includes a large number of users working with a relatively small set of data, collisions are more likely than under the optimistic concurrency scheme. Even if the likelihood of collision is low, the cost of resolving a concurrency conflict after the users have done their data entry might be very high: even the most reasonable users aren't going to be terribly excited at the prospect of rekeying five screens of data every third time they try to enter data into the system.

In these cases, we can use the Pessimistic Concurrency pattern to control access to resources. Pessimistic concurrency is simple: when a user wants to begin work on a resource, she obtains a lock on that resource. The lock is held until the entire update process is complete, and then released. For the duration of the lock, other users attempting to lock the resource are informed that the resource is unavailable. The lock-holder is assured that no underlying changes will occur during the edit process.

In web applications where data entry for a transaction might be spread across several pages and multiple requests (and, most important, across time), the pessimistic locking approach makes a lot of sense: multipage web-based edits can be particularly vulnerable to information being modified from several sources at once—imagine two users accessing an edit system that uses five separate pages to edit different elements of a composite entity (or see Chapter 6). You should be able to see at least two potential conflicts, regardless of whether the data to be edited is loaded at the start of the edit process or on a page-by-page basis.

Pessimistic locking requires much more care than optimistic locking, particularly in a web environment. It's very easy for a user to lock a resource (perhaps a highly contentious one) and then walk away and have lunch. Depending on the architecture you use, the lock might exist forever: if the session times out, the user will never get to the point in the flow where the lock is released. For this reason, locks used with pessimistic concurrency schemes have time limits attached; you must either re-request the lock within a set amount of time, or the lock is released and your work is not saved. This kind of lock is often referred to as a lease. Support for leases should be implemented within the locking framework rather than the application logic itself.

    [ Team LiB ] Previous Section Next Section