←Part 30  Part 32→

The HP e3000--For Complete Novices,
Part 31: IMAGE/SQL Application Logic--Locking Strategies

by George Stachnik

Welcome to part 31 of this series of articles on the HP e3000. We are nearly at an end of our discussion of the IMAGE/SQL database management system. We've been discussing the various intrinsics that are used to read and write data to and from an IMAGE/SQL database. More recently, we began looking at ways of preventing database corruption. This time, we're going to begin looking at one of the biggest causes of database corruption--poor locking strategy.

To understand what a locking strategy is, consider the simple data entry shown in Figure 1. This figure shows a record layout that's about as simple as you can get. There are only two fields: a savings account balance, and a checking account balance. Let's assume that this record contains the account balances for a married couple--we'll call them George and Monica.

Figure 1: Basic Record

Now, watch what happens when George and Monica go to two different ATM machines at the same time. George and Monica have a total of $1,500 dollars in the bank--$1,000 in savings, and $500 in checking. George is going to transfer $100 from savings to checking. Monica is going to withdraw $100 from checking. When both transactions are complete, there should be $900 in savings, and $500 in checking, for a total of $1,400.

Figure 2 shows what happens inside this poorly designed ATM system when George puts his ATM card into the machine and enters his PIN number. At the top of the figure, we see the balances as they are represented in the database. At the bottom, we see that a copy of the record has been retrieved from the database into the ATM's own internal memory. George has entered his transaction and the ATM has dutifully debited the savings account by $100 (leaving a balance of $900), and credited the checking account by the same amount (yielding a balance of $600).

Figure 2: George Uses the ATM

But while this has been going on, Monica has inserted her ATM card into another machine on the other side of town. Figure 3 shows the data that's in the database (at the top), the data that's in George's ATM's memory (lower left), and the data that's in Monica's ATM's memory (lower right). Note that the data in Monica's ATM's memory reflects the contents of the database. In other words, Monica's ATM doesn't know that George is in the middle of transferring $100 from savings to checking. This, as we'll soon see, is going to cause a lot of trouble.

Figure 3: Monica Inserts Her Card

Figure 4 shows what happens next. Monica tells her ATM to withdraw $100 cash. Dutifully, her ATM machine subtracts $100 from the checking account balance. Meanwhile, over in the lower left hand corner of the figure, George has completed his transaction, and written the updated balances back to the database. The database now thinks that there are $900 in savings and $600 in checking.

Figure 4: The Transactions Conflict

In Figure 5, Monica has completed her transaction, and written her data record back to the database. At this point, there should be $900 in savings, and $500 in checking, for a total of $1,400. Instead, we see savings with a balance of $1,000, and checking with a balance of $400. George's transfer has simply been wiped out, as if it never happened.

Figure 5: George's Transfer Wiped Out

What went wrong? Each ATM machine was processing a transaction without any knowledge of what the other one was up to. Both ATMs were acting on the same data in the same record, but there was no mechanism in place to coordinate the two transactions and keep them out of each other's hair.

In IMAGE/SQL, the mechanism for coordinating database transactions is called locking, and here's how it works. In the ATM example, the problems really started in Figure 3. That's where two different copies of the data in the database were created, with no mechanism to keep them in sync. A good IMAGE/SQL application would not allow the scenario shown in Figure 3 to happen.

When an IMAGE/SQL application retrieves a record from a database for the purpose of updating it, it will typically call for a lock on that data. Application programs do this by calling an intrinsic called DBLOCK. DBLOCK can be used to lock database data at any of three different levels:
  • at the RECORD level: By locking data at the record level, an application program effectively prevents other programs (or other users running the same program) from acting on the same record at the same time.
  • at the DATASET level: By locking data at the dataset level, an application program effectively prevents other programs (or other users running the same program) from acting on any record in the same dataset at the same time.
  • at the DATABASE level: By locking data at the database level, an application program effectively prevents other programs (or other users running the same program) from acting on any record in the same database at the same time.
To understand how locking works, let's walk through a scenario. Let's go back to Figure 2. George has retrieved a copy of the data in the database, but this time, let's suppose that he has also used the DBLOCK intrinsic to call for a record-level lock on the record in the database. By holding that lock, George is effectively saying, "I'm using this record. If anybody else comes along wanting to use this record, they're going to have to wait until I'm done."

Now suppose that Monica comes along and asks to update the record (before George is done). But this time, Monica asks to lock the record before she tries to update it. Her request to lock the record will be denied because George is already holding the lock. What happens next depends on how the application that George and Monica are using is coded. The programmer could set it up so that Monica will simply queue up behind George and wait until George completes his transaction before she can begin hers. Or, the programmer could display an error message on Monica's ATM (your account is in use--try again later...).

The important thing to understand is that IMAGE/SQL uses locks to prevent George and Monica from getting into the kind of situation that we explained at the beginning of this article. Without locking, database corruption cannot help but result. However, locking comes with its own set of problems.

The first, and most important thing to understand about locking is that it's a "gentleman's agreement." It doesn't actually prevent anybody from updating the database while another transaction is in progress. In other words, suppose that George and Monica are running two different applications. George's application dutifully locks the database record before it begins updating it, when Monica's application comes along. Now suppose that the programmer who designed Monica's application didn't know about locking (or didn't understand how important it was). Monica's program could retrieve the record that George has locked, update it, and write it back to the database--all while George was holding the lock!

The important thing to understand here is that the fact that George is holding a lock on the data in the database does not stop Monica (or anybody else) from reading, writing, updating, or even deleting that data. It only stops Monica (or anybody else) from locking the data. If an errant application program acts on data without locking it, IMAGE/SQL will do nothing to stop it.

The second thing to understand about locking is that it can have severe performance consequences if it's not used properly. One important rule to remember is this: "Never lock around a user dialog." This means that programs should not prompt the user for information while holding a lock. Let's see why.

Suppose George inserts his ATM card into the machine and tells the machine that he wants to transfer money from savings to checking. The ATM machine's application dutifully finds George's record, locks it (using DBLOCK), retrieves it (using DBGET), and displays the account balances on the ATM before asking George how much money he wants to transfer. Now suppose that George sees the amount in his savings account and drops dead of a heart attack (presumably he thought he had more money in the bank). The ATM, having no way of knowing about George's untimely demise, will simply wait for him to enter an amount.

Meanwhile, across town, Monica is inserting her ATM card into another machine, and requesting the same record. When the ATM application tries to lock the record, it won't be able to (because poor dead George is holding the lock across town). If the application has been designed to wait until the lock becomes available, poor Monica could wind up stuck at that ATM machine until the coroner's office sends somebody across town to the ATM machine that George has been using.

To prevent this kind of deadlock, it's good programming practice to avoid locking around user dialog. George's program should retrieve the record--and engage George in a dialog to see what he wants to do. Only after it has been determined that George wants to transfer $100 from savings to checking should the program at George's ATM lock the record, retrieve it again (to ensure that it hasn't been updated during the dialog), update it, and unlock it. The idea is to hold the lock for the shortest possible length of time, in order to avoid hanging up other users who are waiting for locks.

The performance impact of poorly conceived locking strategies can be devastating. If an application program is going to be making changes to a number of records, it may be a good idea to ask for a dataset-level lock--or even a database-level lock. A user who is holding a dataset-level lock will prevent other users (or other programs) from locking any of the records in that dataset, or from locking the dataset itself. Dataset-level locking effectively single-threads transactions, so that only one transaction can be in progress against a particular dataset at a time. Database-level locking is even worse--ensuring that only one transaction can be in progress against a particular database at a time.

You can see that the effects of database-level locking can be very severe. If a program holding a database-level lock engages in some kind of lengthy user dialog, it will effectively prevent any other transaction from being applied against that database until the dialog is complete and the lock freed up. Yet as bad as database-level locking can be, there is an alternative that can be even worse.

Let's imagine an application that needs to update two different records in order to complete a transaction. We'll call them record A and record B. Now let's assume that two different users are running the application at the same time. We'll call them USER1 and USER2. Let's see what could happen if our two users decide to try to update the same two records at the same time. Remember, both USER1 and USER2 want to lock record A and record B.

Suppose that USER1 issues a request to lock record A, and gets it. But when USER1 tries to lock record B, he finds it's already locked by somebody else--so he waits. Now suppose that record B is locked by USER2, who has issued a request also to lock Record A. Of course, USER2 cannot lock record A because USER1 is already holding that lock.

This scenario is called a "deadly embrace" because the two users are waiting on one another. USER1 cannot lock record B because USER2 has it. USER2 cannot unlock record B because he's waiting for record A, which is locked by USER1. USER1 cannot unlock record A because he's waiting for record B, which is locked by USER2. The two users will literally sit there waiting for one another until doomsday (or until somebody reboots the 3000--whichever comes first).

The best way to avoid deadly embrace situations is to avoid scenarios in which your application requests multiple locks. If this isn't possible (and it often isn't), then your application needs to request locks in a specific order--and everyone needs to observe the same order. The problem we just explained was caused because USER1 and USER2 were trying to lock the two different records in different orders:

USER1 had locked record A and was trying to lock record B.

USER2 had locked record B and was trying to lock record A.

If both users had tried to lock the two different records in the same order, (first A, then B), they wouldn't have gotten themselves into the deadly embrace situation.

Locking is a necessary part of any IMAGE/SQL application in which multiple users are updating data simultaneously. Here are some of the rules we've learned about locking:

  • Always lock at the lowest level possible--record-level locking is better than dataset-level, which is better than database-level.
  • Dataset-level locking may be necessary in situations in which you are updating multiple records (as is the case, for example, when you're updating a chain of records).
  • Never hold a lock while engaging in a user dialog.
  • Use caution when holding multiple locks to avoid deadly embrace situations.

In the next article, we'll look at some programming examples of locking.

George Stachnik works in Hewlett-Packard's Commercial Systems Division in Cupertino, California. He is responsible for the development and delivery of e3000 training.
  ←Part 30  Part 32→
     [3khat]3kRanger   [3khat]3kMail   Updated