Transaction Management
ACID, concurrency problems, serializability, locking and 2PL, and database recovery with logs and checkpoints.
Contents
- Function and importance of transactions
- Properties of transactions (ACID)
- Concurrency control and concurrency problems
- Meaning of serializability; how locking (2PL) ensures it; deadlock
- Database recovery: failures, log file, checkpointing
Transactions
A transaction is an action, or series of actions, carried out by a user or application, which reads or updates the contents of the database.
- A transaction is a logical unit of work on the database.
- It transforms the database from one consistent state to another, although consistency may be violated during the transaction.
Example transactions (DreamHome)
(a) Give a pay rise
read(staffNo = x, salary)
salary = salary * 1.1
write(staffNo = x, new_salary)(b) Delete a staff member
delete(staffNo = x)
for all PropertyForRent records, pno
begin
read(propertyNo = pno, staffNo)
if (staffNo = x) then
begin
staffNo = newStaffNo
write(propertyNo = pno, staffNo)
end
endTransaction (b) touches many rows. If it fails half-way, some properties would point to a deleted staff member — the database would be inconsistent. Hence the need for transaction support.
Outcomes of a transaction
- Success → the transaction commits and the database reaches a new consistent state.
- Failure → the transaction aborts, and the database must be restored to the consistent state before it started. Such a transaction is rolled back (undone).
- A committed transaction cannot be aborted. If it was a mistake, a compensating transaction must be run to reverse its effects.
- An aborted transaction that is rolled back can be restarted later.
State transition diagram
| State | Meaning |
|---|---|
| ACTIVE | The transaction is executing its reads and writes. |
| PARTIALLY COMMITTED | After the final statement has executed. It may still be found to violate serializability or an integrity constraint, or the system may fail before updates are safely recorded → FAILED. |
| COMMITTED | Successful; updates are safely recorded. |
| FAILED | Cannot be committed, or aborted while ACTIVE (user abort, or concurrency control aborts it to ensure serializability). |
| ABORTED | Rolled back; database restored to its prior state. |
ACID properties
| Property | Meaning | Responsible subsystem |
|---|---|---|
| Atomicity | “All or nothing” — an indivisible unit, performed entirely or not at all. | Recovery subsystem |
| Consistency | Must transform the database from one consistent state to another. | DBMS (constraints) and application developers |
| Isolation | Partial effects of incomplete transactions must not be visible to other transactions. | Concurrency control subsystem |
| Durability | Effects of a committed transaction are permanent and must not be lost because of later failure. | Recovery subsystem |
The DBMS enforces declared constraints, but if a programmer’s transfer transaction debits one account and credits the wrong account, every constraint is still satisfied — the DBMS can’t detect the logic error.
Concurrency control
Concurrency control is the process of managing simultaneous operations on the database without having them interfere with one another.
- Prevents interference when two or more users access the database concurrently and at least one is updating.
- Two transactions may each be correct on their own, but interleaving their operations may produce an incorrect result.
Three classic problems: Lost update Uncommitted dependency Inconsistent analysis
Lost update problem
A successfully completed update is overridden by another user.
T1 withdraws £10 from an account with balx = £100; T2 deposits £100 into the same account. Run serially, the final balance would be £190.
| Time | T1 | T2 | balx |
|---|---|---|---|
| t1 | begin_transaction | 100 | |
| t2 | begin_transaction | read(balx) | 100 |
| t3 | read(balx) | balx = balx + 100 | 100 |
| t4 | balx = balx − 10 | write(balx) | 200 |
| t5 | write(balx) | commit | 90 |
| t6 | commit | 90 |
Both read £100. T2 writes £200, then T1 overwrites it with £90 — T2’s £100 deposit is lost.
Fix: prevent T1 from reading balx until after T2’s update is complete.
Uncommitted dependency (dirty read) problem
Occurs when one transaction can see intermediate results of another transaction before it has committed.
| Time | T3 | T4 | balx |
|---|---|---|---|
| t1 | begin_transaction | 100 | |
| t2 | read(balx) | 100 | |
| t3 | balx = balx + 100 | 100 | |
| t4 | begin_transaction | write(balx) | 200 |
| t5 | read(balx) | ⋮ | 200 |
| t6 | balx = balx − 10 | rollback | 100 |
| t7 | write(balx) | 190 | |
| t8 | commit | 190 |
T4 updates balx to £200 but then aborts, so balx should return to £100. But T3 has already read the new value (£200) — dirty data — and uses it for its £10 reduction, giving £190 instead of £90.
Fix: prevent T3 from reading balx until after T4 commits or aborts.
Inconsistent analysis problem
Occurs when a transaction reads several values but a second transaction updates some of them during the execution of the first. Sometimes called a dirty read or unrepeatable read.
T6 totals balances x (£100), y (£50), z (£25) → correct total £175. Meanwhile T5 transfers £10 from balx to balz.
| Time | T5 | T6 | balx | baly | balz | sum |
|---|---|---|---|---|---|---|
| t1 | begin_transaction | 100 | 50 | 25 | ||
| t2 | begin_transaction | sum = 0 | 100 | 50 | 25 | 0 |
| t3 | read(balx) | read(balx) | 100 | 50 | 25 | 0 |
| t4 | balx = balx − 10 | sum = sum + balx | 100 | 50 | 25 | 100 |
| t5 | write(balx) | read(baly) | 90 | 50 | 25 | 100 |
| t6 | read(balz) | sum = sum + baly | 90 | 50 | 25 | 150 |
| t7 | balz = balz + 10 | 90 | 50 | 25 | 150 | |
| t8 | write(balz) | 90 | 50 | 35 | 150 | |
| t9 | commit | read(balz) | 90 | 50 | 35 | 150 |
| t10 | sum = sum + balz | 90 | 50 | 35 | 185 | |
| t11 | commit | 90 | 50 | 35 | 185 |
T6 read balx before the transfer (100) and balz after it (35), so the £10 is counted twice: result £185 — £10 too high.
Fix: prevent T6 from reading balx and balz until after T5 has completed its updates.
Serializability
- The objective of a concurrency control protocol is to schedule transactions so as to avoid interference.
- We could run transactions serially, but this limits the degree of concurrency/parallelism.
- Serializability identifies those executions of transactions guaranteed to ensure consistency.
| Term | Definition |
|---|---|
| Schedule | A sequence of reads/writes by a set of concurrent transactions. |
| Serial schedule | Operations of each transaction are executed one after the other, with no interleaving. |
| Nonserial schedule | Operations from concurrent transactions are interleaved. |
| Serializable schedule | A nonserial schedule that is equivalent to some serial schedule. |
The objective of serializability is to find nonserial schedules that allow transactions to execute concurrently without interfering — i.e. that produce the same result as some serial execution.
When does order matter?
- If two transactions only read a data item → no conflict, order not important.
- If two transactions read or write completely separate data items → no conflict, order not important.
- If one transaction writes a data item and another reads or writes the same item → order is important (conflict).
A conflict needs same item + different transactions + at least one write: R–W, W–R, W–W.
Conflict serializability example
| Time | S1: T7 | S1: T8 | S3 (serial): T7 | S3: T8 |
|---|---|---|---|---|
| t1 | begin | begin | ||
| t2 | read(balx) | read(balx) | ||
| t3 | write(balx) | write(balx) | ||
| t4 | begin | read(baly) | ||
| t5 | read(balx) | write(baly) | ||
| t6 | write(balx) | commit | ||
| t7 | read(baly) | begin | ||
| t8 | write(baly) | read(balx) | ||
| t9 | commit | write(balx) | ||
| t10 | read(baly) | read(baly) | ||
| t11 | write(baly) | write(baly) | ||
| t12 | commit | commit |
In S1 (nonserial), every conflicting pair (on balx and on baly) has T7 before T8 — the same order as the serial schedule S3. By swapping non-conflicting operations (e.g. T8’s balx ops with T7’s baly ops) S1 can be transformed into S2 and then S3. So S1 and S2 are conflict serializable.
A schedule that orders any conflicting operations in the same way as some serial execution.
Draw a node per transaction and an edge Ti → Tj whenever an operation of Ti conflicts with and precedes one of Tj. The schedule is conflict serializable iff the graph has no cycle. In the lost-update schedule, T1 reads before T2 writes (T1→T2) and T2 reads before T1 writes (T2→T1): a cycle → not serializable.
Concurrency control techniques
- Two basic techniques: locking and timestamping.
- Both are conservative (pessimistic) approaches: they delay transactions in case they conflict.
- Optimistic methods assume conflict is rare and only check for conflicts at commit.
Locking
- A transaction uses locks to deny access to other transactions and so prevent incorrect updates.
- The most widely used approach to ensure serializability.
- A transaction must claim a shared (read) or exclusive (write) lock on a data item before reading or writing it.
- A lock prevents another transaction from modifying the item — or even reading it, in the case of a write lock.
Basic rules
- Shared lock (S) → can read but not update the item.
- Exclusive lock (X) → can read and update the item.
- Reads cannot conflict, so many transactions can hold shared locks on the same item simultaneously.
- An exclusive lock gives a transaction exclusive access to the item.
- Some systems allow upgrading a shared lock to exclusive, or downgrading exclusive to shared.
Lock compatibility matrix
| Ti holds \ Tj requests | S | X |
|---|---|---|
| S | ✅ true | ❌ false |
| X | ❌ false | ❌ false |
Only S + S is compatible. Locks must be held for the duration of access.
Two-Phase Locking (2PL)
A transaction follows the 2PL protocol if all locking operations precede the first unlock operation in the transaction.
- Growing phase — acquires all locks but cannot release any.
- Shrinking phase — releases locks but cannot acquire new ones.
- Ensures serializability, but not freedom from deadlocks.
- May seriously reduce parallelism.
It can be proved that if every transaction in a schedule follows 2PL, the schedule is guaranteed to be conflict serializable.
Preventing lost update with 2PL
| Time | T1 | T2 | balx |
|---|---|---|---|
| t1 | begin_transaction | 100 | |
| t2 | begin_transaction | write_lock(balx) | 100 |
| t3 | write_lock(balx) | read(balx) | 100 |
| t4 | WAIT | balx = balx + 100 | 100 |
| t5 | WAIT | write(balx) | 200 |
| t6 | WAIT | commit/unlock(balx) | 200 |
| t7 | read(balx) | 200 | |
| t8 | balx = balx − 10 | 200 | |
| t9 | write(balx) | 190 | |
| t10 | commit/unlock(balx) | 190 ✓ |
T2 obtains an exclusive lock first; T1’s lock request is not granted, so T1 waits until T2 commits and releases the lock.
Preventing uncommitted dependency with 2PL
| Time | T3 | T4 | balx |
|---|---|---|---|
| t1 | begin_transaction | 100 | |
| t2 | write_lock(balx) | 100 | |
| t3 | read(balx) | 100 | |
| t4 | begin_transaction | balx = balx + 100 | 100 |
| t5 | write_lock(balx) | write(balx) | 200 |
| t6 | WAIT | rollback/unlock(balx) | 100 |
| t7 | read(balx) | 100 | |
| t8 | balx = balx − 10 | 100 | |
| t9 | write(balx) | 90 | |
| t10 | commit/unlock(balx) | 90 ✓ |
T3 cannot read balx until T4’s rollback has completed and released the lock, so it reads the correct value (100).
Preventing inconsistent analysis with 2PL
T5 precedes its reads with exclusive locks; T6 precedes its reads with shared locks. When T5 starts it obtains X on balx; T6’s S-lock request on balx must WAIT until T5 commits and unlocks balx and balz. T6 then reads 90 + 50 + 35 = 175 ✓.
Deadlock
A deadlock is an impasse where two (or more) transactions each wait for a lock held by the other — e.g. T1 holds X(balx) and wants baly; T2 holds X(baly) and wants balx. Neither can proceed.
- Resolution: the DBMS detects it (e.g. a cycle in the wait-for graph), aborts one transaction (the victim), rolls it back and restarts it later.
- Other approaches: timeouts (abort after waiting too long); prevention using timestamps (wait-die, wound-wait).
Database recovery
Database recovery is the process of restoring the database to a correct state in the event of a failure.
Need for recovery control
- Two types of storage: volatile (main memory) and non-volatile (disk).
- Volatile storage does not survive system crashes.
- Stable storage = information replicated on several non-volatile media.
Types of failure
- System crashes → loss of main memory
- Media failures → loss of parts of secondary storage
- Application software errors
- Natural physical disasters
- Carelessness or unintentional destruction of data/facilities
Transactions and recovery
- Transactions are the basic unit of recovery.
- The recovery manager is responsible for atomicity and durability.
- If failure occurs after commit but before the buffers are flushed to disk → to ensure durability the recovery manager must REDO (roll forward) the transaction’s updates.
- If the transaction had not committed at failure → to ensure atomicity it must UNDO (roll back) the transaction’s effects.
- Partial undo — only one transaction has to be undone. Global undo — all transactions have to be undone.
Example
A bar ending in | = committed. Orange = still active at the crash; blue = committed between the checkpoint and the crash.
The DBMS starts at t0 and fails at tf. Assume data for T2 and T3 have been written to secondary storage.
| Scenario | UNDO | REDO | No action |
|---|---|---|---|
| No checkpoint | T1, T6 (active at crash) | T2, T3, T4, T5 (all committed — recovery manager can’t know which were flushed) | — |
| Checkpoint at tc | T1, T6 | T4, T5 (committed after tc) | T2, T3 (committed before tc, already on disk) |
Recovery facilities
The DBMS should provide:
- Backup mechanism — makes periodic backup copies of the database.
- Logging facilities — keep track of the current state of transactions and database changes.
- Checkpoint facility — enables updates in progress to be made permanent.
- Recovery manager — restores the database to a consistent state after a failure.
Log file
Contains information about all updates to the database: transaction records and checkpoint records. Often used for other purposes too (e.g. auditing).
Transaction records contain:
- Transaction identifier
- Type of log record (transaction start, insert, update, delete, abort, commit)
- Identifier of the data item affected (for insert, delete and update)
- Before-image of the data item (used for UNDO)
- After-image of the data item (used for REDO)
- Log management information (pointers to previous/next record of the same transaction)
Sample log file
| Tid | Time | Operation | Object | Before image | After image | pPtr | nPtr |
|---|---|---|---|---|---|---|---|
| T1 | 10:12 | START | 0 | 2 | |||
| T1 | 10:13 | UPDATE | STAFF SL21 | (old value) | (new value) | 1 | 8 |
| T2 | 10:14 | START | 0 | 4 | |||
| T2 | 10:16 | INSERT | STAFF SG37 | (new value) | 3 | 5 | |
| T2 | 10:17 | DELETE | STAFF SA9 | (old value) | 4 | 6 | |
| T2 | 10:17 | UPDATE | PROPERTY PG16 | (old value) | (new value) | 5 | 9 |
| T3 | 10:18 | START | 0 | 11 | |||
| T1 | 10:18 | COMMIT | 2 | 0 | |||
| 10:19 | CHECKPOINT | T2, T3 | |||||
| T2 | 10:19 | COMMIT | 6 | 0 | |||
| T3 | 10:20 | INSERT | PROPERTY PG4 | (new value) | 7 | 12 | |
| T3 | 10:21 | COMMIT | 11 | 0 |
Notice: an INSERT has only an after-image; a DELETE has only a before-image. The checkpoint record lists the transactions active at that moment (T2, T3).
Checkpointing
A point of synchronization between the database and the log file. All buffers are force-written to secondary storage.
- A checkpoint record is created containing the identifiers of all active transactions.
- When failure occurs: redo all transactions that committed since the checkpoint, and undo all transactions active at the time of the crash.
- In the example with a checkpoint at tc, changes by T2 and T3 are already on disk, so only redo T4 and T5, and undo T1 and T6.