|
|
CIS 2210 - Database Management and Design
Chapter 10: Transaction Management and Concurrency Control
Objectives:
This lesson discusses material from chapter 10. Objectives
important to this lesson:
- Transactions
- Concurrency control
- Locking methods
- Timestamping methods
- Optimistic methods
- ANSI levels of isolation
- Recovery management
Concepts:
Chapter 10
Transactions
The chapter begins with a
diagram of six relations on page 483. It shows that they have five
1-to-many relationships, the "many" being represented by infinity
symbols in this case. The text begins to describe what a transaction
is, using this set of tables as a background. When a customer makes a
purchase from this company, there are a
series of events that take
place, updating several tables. The entire
series of related events can
be considered as an example of a transaction. From the customer's point
of view, one thing happened: a purchase took place. From the company's
point of view several things had to happen:
- an invoice had to be generated, affecting the Invoice,
Line, and Customer tables (where the balance is stored)
- the product inventory had to be adjusted, affecting the
Product table
- the customer's order was logged in the Account_Transaction
table as a new record
We can take the book's suggestion on page 484 that we should think of
a transaction as a logical unit of work
in which all of the steps need to be completed. If a problem keeps some
step from being completed, the steps that were completed need to be undone.
This should make you think about the COMMIT
command, which should be issued before
starting a transaction, and after all
steps in a transaction have been successfully
completed. Why "before starting a transaction"? So that the ROLLBACK
command can be issued if any step
in the transaction fails, making the system forget all those steps. The
text refers to this as maintaining a consistent
database state. There should be no bill for this customer unless
the order can be filled. From an accounting perspective, we do not increase
our assets in accounts payable,
unless we decrease our assets
in our inventory. The books have
to balance. (Yes, the customer's request could be placed on a Waiting_List
table, but that is not part of this example.)
The text gives us an example of a transaction that is only a
SELECT query: we ask the database for the customer's balance. This can
be considered a transaction
simply because the consistent
database
state is maintained. This example is a special case: it is easy
to roll it back, since it is only one command. Most transactions take
more than one SQL command, which means that we have to be careful about
where each transaction starts and ends. The number of steps in a transaction is
measured by the number of database
requests (commands) that are actually used. The text reminds us
that there may be several operating system calls that happen as a
result of each database request, but these are not counted as parts of the
transaction.
On page 485, the text shows us a transaction example that
inserts lines into three tables, and updates values in two others. Note
the fourth step in this transaction. It is an UPDATE command that
includes this line:
SET CUST_BALANCE = CUST_BALANCE + 277.55
(no semicolon because the command does not end at this clause)
This line would not
work as written if the CUST_BALANCE attribute did not hold numeric data. Remember the rule:
data must be numeric if you are going to do math with that data. The
line does work because we are adding a number to the value held in a
field, and assigning the result to that field.
At the end of that example, the COMMIT command is given. This should
not be done until you are
sure the required changes have been made. The text describes an
electrical problem that would cause one or more steps of a transaction
to fail. It states that a DBMS that supports transaction management
would restore the database to a consistent state in that case by
rolling back the completed transaction steps. To do this manually in MySQL, you should use
COMMIT commands before starting
a transaction and upon ending
it. You may also use the command START TRANSACTION
to mark the beginning of a transaction sequence. MS SQL server uses the
phrase BEGIN TRANSACTION. Access does not directly support transaction
management.
On page 487, the text introduces some terms that describe characteristics
of a good transaction:
- Atomicity - The steps
of a transaction are all required
to take place. This means that this set of steps must have been simplified
as much as possible, that no steps can be removed from the list without
changing the nature of the transaction itself. This is atomicity:
the state in which removing any part
would change the transaction into something else. In atomic physics,
if you remove an electron or a proton from an atom, that atom becomes
something different than it was. The same applies to any process that
displays atomicity.
- Consistency - The
quality of preserving a consistent state in the database. If a
transaction does not preserve a consistent state, it is not a good
transaction and it should be rewritten.
- Isolation - A
transaction should not access
data currently being accessed
by another transaction. A
transaction should access data in such a way that that data is locked, and unavailable to any other transaction
until the locking transaction has been completed or has been rolled
back. This method ensures that each access to data is isolated from every other access to
data.
- Durability - Once a transaction
is done, it is committed to the database, making it a durable part of
the database. They cannot be rolled back by the DBMS at that point.
This is a more stable situation than waiting until the end of a day
to process all transactions from that day.
Isolation brings up an
issue that must be handled by a DBMS with multiple concurrent users.
When concurrent sessions request access to the same files, the
Isolation characteristic requires that the transactions be queued to happen one after another.
The text refers to ability to queue transaction in this way as serializability. It means that the
transactions can be carried out one at a time, typically ordered by
their timestamps. Serializability
supports Isolation.
On page 488, the text reviews the use of COMMIT and ROLLBACK
under normal circumstances, and under two less normal ones:
- If a program that starts a transaction ends normally, but without calling a
COMMIT command, changes to the database are saved as though a COMMIT
command had been given.
- If a program that starts a transaction ends abnormally, before a COMMIT command
has been given, the system should
issue a ROLLBACK command, removing any pending changes from
consideration. (Do you feel a little funny now about just closing a
browser window when shopping on the Internet?)
- The text notes that you should always close a transaction
with a COMMIT command, instead of depending on the automatic commit
expected at the close of a script.
The text describes a useful feature of transaction management,
the transaction log. This is a
running file that holds all steps of all transactions that change the
database. This feature is required by an ANSI standard to allow rolling
back incomplete transactions and rolling forward from completed
transactions through any pending transactions that are still stored in
the log. Naturally, log files can and should be discarded after a time,
but they should be retained for reconstructing the state of a database
as needed. Note the example transaction log on page 490. It does not hold actual SQL commands, but it
holds information that could be used to reconstruct the commands that
it represents. Why do you think that this is so?
Concurrency Control
On page 490, the text defines concurrency
control as the coordination
of the actions of multiple users of a database system. As noted above,
transactions must be serialized to work in such an environment. When
they are not, problems occur:
- lost update problem - When
two transactions are updating the same data element (such as a customer's
balance) coordination is vital. Assume two transactions read a customer's
record at the same moment. One takes an order
and increases the current balance.
The other takes a payment and
decreases the customer's balance.
Now imagine that they each read the customer's record before the other
transaction updates the balance.
Transaction
|
Action
|
| Sale |
Reads
customer's balance: 100.00
|
Payment
|
Reads
customer's balance: 100.00 |
| Sale |
Calculates
new balance: balance = balance + 50.00
Balance = $150.00
|
| Payment |
Calculates
new balance: balance = balance - 50.00
Balance = 50.00
|
| Sale |
Saves
new balance: balance = 150.00
|
| Payment |
Saves
new balance: balance = 50.00
|
If the actions take place as shown above, the customer may get the new
order, but the store is out the price of it. Should it have happened
this way instead?
Transaction
|
Action
|
Payment
|
Reads
customer's balance: 100.00 |
| Sale |
Reads
customer's balance: 100.00
|
| Payment |
Calculates
new balance: balance = balance - 50.00
Balance = 50.00
|
| Sale |
Calculates
new balance: balance = balance + 50.00
Balance = $150.00
|
| Payment |
Saves
new balance: balance = 50.00
|
| Sale |
Saves
new balance: balance = 150.00
|
This way, we get an unhappy customer who wants to know what happened.
In both cases, the database
is inconsistent with the actual events. This illustrates why transactions
should be isolated as well as sequential. Everything would be fine if
either transaction had started and finished before the other one started.
- uncommitted data problem -
A related problem occurs when one transaction reads data, saves a new
value, then rolls back, while another transaction reads the new saved
value, calculates a change to it, then saves its new change, oblivious
to the fact that the number it is working from is a false value. An
example is shown on page 492. The first transaction should never have
written its change before it was done. Having done so, the second transaction
read a number that was later rolled back. We get the same kind of chaos
as in the example above. No transaction should be allowed to start regarding
data that is in flux with another transaction. Wait your turn, children.
You will be happier that way.
- inconsistent retrievals problem
- This one comes from reading a table
that is being updated while
you are reading it. You get some old
data, some new data, then perform
an operation on your faulty dataset, resulting in a faulty interpretation
of the data. This one is tricky. If you think about it, you could defend
the idea that the data looked the way you read it at a specific moment
in time. This is not a good argument if the changes being made were
corrections, but what if they were ongoing live data? Would it make
sense then? Trust that the concept is correct: don't read tables while
they are being changed. At the very least, you are taking measurements
at more than one moment, so you are not comparing different lines in
the same timeframe. Taking any aggregate measure on a database should
not be considered if that database is being updated. A database can
only be consistent before updates
and after updates, never
during updates.
To address the problems discussed above, a DBMS typically has a scheduler,
an organizing function that attempts to put the actions of concurrent
transactions in something that approximates a proper serial order. You
should be frightened a bit by the text's description of the scheduler
interleaving the tasks of the
various transactions. The word interleaving may make you think of shuffling
a deck of cards. It is less like that, and more like stacking
a deck of cards to make sure that players get particular hands. That's
not a good idea at a poker table, but it's a very good idea in a DBMS.
Even though the tasks are not done sequentially for each transaction,
they are done in a way that is meant to accomplish the same result as
if they had been done in an ideal sequence.
So, why does it do this, when we know that it is unsafe to try
it? It does this to make better use of the hardware and software in the
system, and to move ahead on each transaction as much as possible. Note
that: "as much as possible". It is often necessary to wait for a lock
to resolve. We discuss locks below.
Locking Methods
We have almost discussed the idea of locking data several times. Now it is time to define some ideas about it. A lock is like a tag that says only a particular process is granted access to a data object until that access is released. A pessimistic lock
is a lock that is placed on a data object with the assumption that
another process is likely to cause a problem. A lock should be placed on data before reading it, held on that data while changing it, and released only after the data changes are committed. A lock manager is the part of a DBMS that that automatically applies locks and constrains other processes from ignoring them.
A lock can be placed on any of several kinds of data objects. The level of precision of a lock is called its granularity. For instance, a lock may be placed on an entire database, on a single table, on a row in a table, or on a field in a row. Each of these examples is more granular than the ones before it. The text also mentions a page lock, which restricts a section of memory, as opposed to a section of a database.
Lock level
|
Details
|
Database
|
The entire database is locked for one process. Good for a batch update, but not good for sharing among many users. |
| Table |
Locks
an entire single table. Only one process can access it, but another
process can access a different table. Not very useful when users need
to update multiple tables in a single transaction.
|
| Page |
Locks
a section of the file that fills a portion of memory, commonly a 4KB
block. This is more useful than the table lock when you have large
tables, but less useful than a row lock.
|
| Row |
Locks a single row at a time in a table. Multiple transactions can access different rows at the same time. Can lead to a deadly embrace/deadlock if the database is not clever enough to avoid it.
|
| Field |
Locks
only a single field in a single row of a table. Very flexible, but
takes a lot of processing to manage it for more than a few users.
|
Each of these lock levels may have a lock type/mode as well. The text mentions two:
- binary - If a lock is binary, it can only be locked or
unlocked. If an object is locked, only the transaction having the lock
can use the object. If an object is unlocked, any transaction can lock
it. Once a transaction is done with an object, the lock must be
unlocked. Generally considered too limiting for concurrency.
- shared/exclusive - This type allows more choices. An exclusive lock is exclusive to the process that applies the lock. This is typically used by a process that has to change the object. A shared lock is a read only permission, allowing multiple processes to have shared locks one the same object at the same time. In this system, an object can be unlocked, shared, or exclusive. An exclusive lock can only be granted if the object in question is unlocked. A shared lock can be granted if the object is shared or unlocked.
- To make it confusing, the lock manager has three actions in this system: READ_LOCK to read an object's state, WRITE_LOCK to grant a process a lock to an object, and UNLOCK to release a lock currently held on an object.
Locks can cause problems: either removing
serializability, or creating a
deadlock. The text describes methods to avoid both, starting on
page 500.
- two-phase locking - This is a method that addresses serializability.
It does not address deadlocks.
The first phase is the growing phase. The transaction acquires
all locks needed to do its job, one by one. When it has them all, it
is at the stage called the locked point. No actual work is done
until the locked point is reached.
The second phase is the shrinking phase. As you might guess from
the name, all locks held by the transaction are released in this
phase. During this phase, no new locks are allowed.
As noted above, this method does not prevent deadlocks. If two transactions
require the same exclusive locks, and each has obtained one or
more of them, both transactions are blocked from attaining their
locked points. As the text notes, this can only happen with exclusive
locks. Shared locks have no limit on the number of transactions that
may have them in common.
- deadlock prevention - If the system notices that a deadlock
is possible, the transaction being processed is aborted,
its actions (if any) are rolled back, and its currently held
locks are released. The transaction is rescheduled. If we were practicing
two phase locking, there should have been no actions from this transaction
yet.
- deadlock detection - If a deadlock is detected (already
happening), one of the transactions is chosen as the victim.
This one is aborted and rolled back. The other transaction is allowed
to continue. The victim transaction is restarted.
- deadlock avoidance - According
to this site, the method in this case is to grant access only to
those resources that cannot cause a deadlock. This makes more sense
than the text, which is a better description of two-phase locking. Follow
that link, by the way, for a little different spin on the other two
as well.
Timestamping Methods
Timestamping is a method often
used with network compatible applications. In this case, the DBMS reads
the current system time when new requests are received, converts it to
a global time, and each request is given a unique timestamp. Timestamps
must be unique to ensure that
each request has a unique spot in a queue. The text refers to timestamps
as being global. It means that
the same time system is in effect regardless of the physical location
of the requester. Often, timestamping is done with reference to Greenwich
Mean Time (GMT) or Universal Time Coordinated (UTC), which seems to have
been renamed as Coordinated Universal Time. As the page
behind this link explains, GMT and UTC represent the same time, the
time kept by the Royal Observatory at Greenwich, England. (Longitudes
are measured as being so far west (negative values) or east (positive
values) of the prime meridian which passes through Greenwich.) Despite
other sources' statements to the contrary, GMT does not observe daylight
saving time changes, nor does UTC. This provides a system of timekeeping
for a network that covers the Earth.
The text gives us a new word that goes along with timekeeping: monotonicity.
It is a noun. It means the quality of either never increasing or never
decreasing. With reference to timestamps, it means never
decreasing. Each timestamp applied to a new request must be greater
than/later than the timestamps applied to all previous requests. Time
flows forward on a network, never backward. (And in the real world, so
far.) This quality supports the requirement that all timestamps be unique.
Getting back to the subject, the text tells us that steps in a transaction
can all receive the same timestamp, because they are parts of the same
request. However, subsequent transactions will have later timestamps.
This allows different transactions to have timestamps that put then in
an order that does not change. This order is serialized.
As discussed above, transactions can still become in conflict with each
other. When this occurs, the transaction that is stopped, rolled back,
and restarted is given a new timestamp, which will put it farther away
from the one that continued. This produces more overhead for the DBMS.
The text discusses two methods of managing timestamp systems. Each has
two possible rules based on which transaction requests a lock:
- the wait/die scheme is like a first come, first served system regarding
requests for locks
- The transaction with the earlier
timestamp is requesting a lock that would cause a problem:
The later transaction is favored.
The transaction with the earlier
timestamp waits for the other
transaction to finish and release its locks. Then execution of the
transaction with the earlier
timestamp continues.
- The transaction with the later
timestamp is requesting a lock that would cause a problem:
The earlier transaction is
favored.
The transaction having the later
timestamp is chosen to die.
It is stopped, rolled back, and rescheduled with its same timestamp.
The transaction with the earlier
timestamp continues.
- the wound/wait scheme -
- The transaction with the earlier
timestamp is requesting a lock that would cause a problem:
The earlier transaction is
favored.
The earlier transaction wounds/preempts
the later one. The later transaction
is stopped, rolled back, and rescheduled with its same timestamp.
- The transaction with the later
timestamp is requesting a lock that would cause a problem:
The earlier transaction is
favored.
The transaction with the later
timestamp waits for the other
transaction to finish and release its locks. Then execution of the
transaction with the later
timestamp continues.
As you can see, these two methods take similar actions, but they work
in different ways. Only the wait/die method has a circumstance that favors
the later transaction (when the older transaction is asking for a lock).
All other circumstances favor the earlier transaction. To improve processing
flow, the text mentions one other action: if any transaction requests
a lock, and that lock is not granted within an associated time limit,
the transaction is rolled back and started again.
Optimistic Methods
Back in the beginning of the chapter, we were introduced to the idea
of a pessimistic lock. The text takes a different approach next. What
if we assume that most locks do not actually conflict with each other?
This is an optimistic approach.
With this approach we do not use locks or timestamps. (Are you afraid
yet?) Transactions have three phases instead:
- Read - Read the database, make a dataset, make changes to the dataset.
- Validation - Check whether changes made to the dataset would create
integrity or consistency errors in the database. If no errors (positive
test), proceed to the next phase. If errors are detected (negative test),
discard the dataset and restart the transaction.
- Write - This phase is only entered if no errors are found in the validation
phase. That being so, changes are written from the dataset to the database.
The text warns us that this approach will work only if there are few
changes made to the data in the database in question. If there are frequent
changes, locks and timestamps should be used.
ANSI Levels of Transaction Isolation
In addition to the methods discussed above, the ANSI standard uses different
terms to describe amount of isolation,
protection of transaction data from other transactions. It begins by defining
three kinds of reads that a transaction might allow other
transactions to make:
- dirty read - another transaction can read data that is not yet committed
(saved)
- nonrepeatable read - reads of the same row at different times give
different results, because the row may have been changed or deleted
- phantom read - a query run at two different times produces more matching
rows at the later time
In each case, there is an inconsistency with these reads. The ANSI standard
provides four levels of increasing restriction, each one providing more
isolation than the last.
ANSI level
|
Allows
Dirty Read?
|
Allows
Nonrepeatable Read?
|
Allows
Phantom Read?
|
Isolation/Security
Rating
|
Read
Uncommitted
|
Yes |
Yes |
Yes |
bad
|
Read
Committed
|
No |
Yes |
Yes |
better
|
Repeatable
Read
|
No |
No |
Yes |
good
|
| Serializable |
No |
No |
No |
best
|
This is in keeping with the rest of the material in this chapter. The
text explains that we can have more isolation/restriction/security, but
if we do, we reduce the amount of concurrency we can have. Its a trade
off. ANSI SQL allows a transaction to have its isolation level set when
it is begun, as in this example:
BEGIN TRANSACTION ISOLATION LEVEL
READ COMMITTED
… SQL STATEMENTS….
COMMIT TRANSACTION;
A MySQL command to do this might look like this:
START TRANSACTION WITH
READ COMMITTED
… SQL STATEMENTS….
COMMIT TRANSACTION;
Recovery Management
The last section in the chapter discusses database recovery management.
We have considered several times in this chapter how to recover from a
failed transaction. The question in this discussion is how to recover
from a failed database. The text begins by discussing events that could
cause this kind of failure:
- hardware failure
- software failure
- human error or intentional human intervention
- physical disaster, such as fire, flood, power failure (The text calls
this one natural disaster, but there is nothing natural about a power
failure.)
When a recovery is needed, it may rely on one or more of these features:
- write-ahead-log protocol -
This tells the DBMS to write down a transaction's details in a log before
the transaction is processed. The idea is that a system failure that
takes place during the transaction will not change our being able to
roll back or complete the necessary transaction.
- redundant transaction logs
- This means to store backup copies of the logs in different places,
each of which is unlikely to suffer from a disaster that wipes out the
others.
- database buffers - These are
copies of parts of the database that are used as draft copies of the
data a transaction is working on. It is much faster to change a copy
in RAM, then write several changes at once, than it is to write every
change to the database as it happens. It is the same idea as using a
dataset to hold changes that are written to the database when they are
all ready.
- database checkpoints - When
all open buffers are written at once to the disk version of the database,
that is a database checkpoint. Checkpoints need to be run regularly
enough to provide accurate live data to the database. When a checkpoint
has run, it is recorded in the transaction
log.
Okay, with those features understood, the text continues on page 507
to discuss two kinds of recovery. Both kinds assume that you restored
your database to its last working version.
- The first type is a deferred-write,
also called deferred update)
recovery. This is done on systems that update the transaction log immediately,
but the do not update the database immediately. The text gives us four
steps to conduct this kind of recovery:
- Find the last checkpoint in the transaction log. This is the last
time the database was updated.
- Ignore transactions that were completed before the last checkpoint.
They were written during that checkpoint.
- For any transaction that was committed after the last checkpoint,
use the DBMS to reconstruct that transaction from the transaction
log.
- Ignore transactions that were rolled back or still in process after
the last checkpoint.
- The second type is a write-through
(also called an immediate update)
recovery. This is done on systems that update the database as transactions
are processed, before they are committed. On such systems, if there
is an aborted transaction, that transaction needs to be rolled back.
That makes this one different.
- Find the last checkpoint in the transaction log, as above.
- Ignore transactions that were completed before the last checkpoint.
They were written during that checkpoint.
- For any transaction that was committed after the last checkpoint,
use the DBMS to reconstruct that transaction from the transaction
log.
- For any transaction that was rolled back after the last checkpoint,
or had not reached its COMMIT point, use the DBMS to roll back or
undo each of them, up to the checkpoint.
The chapter ends with a walkthrough of an example transaction log.
|