Chapter 06

Locking, 2PL, Isolation & Deadlock

The pessimistic answer to concurrency: locks, the two-phase rule that makes them sufficient, strictness that makes aborts safe, the SQL isolation levels built from them, and what to do when transactions wait for each other forever — including Obermarck's distributed detection and hierarchical intention locks.

Reading: ~55 min Interactive: 1 widgets Source: Polimi Data Base 2 2025/26 — Lecture 3.2 (Concurrency Control), slides 46–102 · Atzeni, Ceri, Fraternali, Paraboschi, Torlone — Basi di dati (2023)

01 · Mechanism

Locks and the conflict table

The most common method in commercial systems, and the pessimistic one: assume collisions will happen, and control access to resources so they cannot.

A transaction is well-formed with respect to locking when every read is preceded by an r_lock (a shared lock) and every write by a w_lock (an exclusive lock), each eventually followed by an unlock. Unlocking may be delayed well past the end of the operation itself — and the whole of this chapter turns on how long it is delayed.

A transaction that reads an object and later writes it has two options: acquire the exclusive lock straight away at read time, or take a shared lock and later upgrade it — lock escalation.

An object is therefore in one of three states — free, r-locked (by one or more readers), or w-locked (by exactly one writer) — and the lock manager grants requests from this table:

Requestfreer-lockedw-locked
r_lockOK → r-lockedOK → r-locked (n++n{+}{+})NO → w-locked
w_lockOK → w-lockedNO → r-lockedNO → w-locked
unlockERROROK → depends (nn{-}{-})OK → free

where nn counts concurrent readers, incremented on each r_lock and decremented on each unlock; the object becomes free only when it reaches zero. Implementations use a hash-indexed lock table: each locked item has a linked list of nodes recording the requesting transaction, the lock mode, and whether it is granted or waiting.

Now the crucial negative result. Consider the arrival sequence r1(x) r2(x) w2(x) r1(x), executed by a scheduler that obeys the table above:

r1(x)  → r_lock granted, n=1;   T1 unlock(x) → free
r2(x)  → r_lock granted, n=1
w2(x)  → upgrade to w_lock granted
         T2 unlock(x) → free
r1(x)  → r_lock granted again

Every request was legal. Yet T1 read x twice and got two different values — a nonrepeatable read. Respecting the lock-granting table is not sufficient for serializability.

What went wrong, precisely

T1 did two things: it released its read lock too early, and then it acquired another lock after having released one. Forbidding the second of those is the entire content of the next section.

02 · Protocol

Two-phase locking

One rule, and the class of schedules it generates is serializable.

The two-phase rule

A transaction cannot acquire any lock after releasing a lock.

Every transaction therefore has a growing phase in which it only acquires, a plateau at its maximum lock set, and a shrinking phase in which it only releases. Consider a scheduler that processes only well-formed transactions, grants locks by the conflict table, and checks the two-phase rule: the class of schedules it produces is called 2PL, and

Result

Schedules in 2PL are both view- and conflict-serializable: 2PL ⊂ CSR ⊂ VSR.

The proof is short and worth reproducing, because papers ask you to justify membership rather than assert it.

Deep dive Why 2PL implies CSR, and why the inclusion is strict

2PL ⇒ CSR. Suppose a 2PL schedule SS were not conflict-serializable. Its conflict graph would contain a cycle, say TiTjTiT_i \to T_j \to \ldots \to T_i, which requires a pair of conflicting operations in reverse order — some OPi(x),OPj(x)OP_i(x), OP_j(x) earlier and some OPj(y),OPk(y)OP_j(y), OP_k(y) later leading back to TiT_i. For TjT_j to access xx after TiT_i, TiT_i must have released a lock. For the conflict on yy to occur later in the other direction, TiT_i must acquire a lock afterwards. That is exactly what the two-phase rule forbids — contradiction. Inclusion in VSR then follows from CSR ⊂ VSR.

CSR ⊋ 2PL. The counter-example is r1(x) w1(x) r2(x) w2(x) r3(y) w1(y). It is conflict- serializable with order T3<T1<T2T_3 < T_1 < T_2 — on x the operations run r1 w1 r2 w2, on y they run r3 w1, and the graph is acyclic. But it violates 2PL: T1 must release its lock on x for T2 to proceed at position 3, and must then acquire a lock on y at position 6. Release then acquire — not two-phase.

So locking is a sufficient discipline, not a characterisation: it rejects some perfectly correct schedules in exchange for being enforceable online, one request at a time.

The lectures also give a visual test that is faster than reasoning in prose: draw resources on the vertical axis and operation times on the horizontal, mark each transaction’s first acquisition and last release per resource, and look for any transaction whose acquisitions are not all before its releases.

Which anomalies does plain 2PL stop? All of them except one:

AnomalyBlocked by 2PL?Why
Nonrepeatable readT1 would have to release then re-acquire
Lost updatesame
Phantom updatesame
Phantom insert⚠️ partlyneeds locks on future data — see predicate locks
Dirty readrequires reasoning about aborts, which 2PL says nothing about

03 · Protocol

Strict 2PL, and why write locks are held to commit

Everything so far assumed commit projection — that no transaction aborts. Drop that assumption and 2PL is not enough.

If a transaction releases its locks before it commits, another transaction can read what it wrote; should the first then roll back, the second has performed a dirty read. The fix adds one clause:

Strict 2PL

Locks held by a transaction may be released only after commit or rollback.

Strict-2PL locks are called long-duration locks; plain 2PL locks are short-duration. Most commercial DBMSs use strict 2PL when a high isolation level is requested. Real systems typically apply it asymmetrically — long-duration strict locks for writes, more varied policies for reads, because long-duration read locks are expensive and are often replaced by the multiversion mechanisms of chapter 7.

Why writes in particular can never be short-duration is worth deriving, since a paper asks it outright:

Deep dive Dirty write — why long-duration write locks are non-negotiable

Suppose write locks could be released early, and consider

w1[x] … w2[x] … then (c1 or a1) and (c2 or a2) in any order

T2 has overwritten an object that T1 has not yet finished with. Now let T1 abort. How should the system process a1?

  • If x is restored to its pre-T1 state, T2’s update is silently destroyed — and if T2 then commits, x holds a stale value that no serial execution produces.
  • If x is not restored, and T2 subsequently aborts too, then T2’s own before-state — which was T1’s uncommitted value — cannot be reinstalled either. Neither rollback can be made correct.

The anomaly is called a dirty write, and the only way out is to prevent the interleaving: write locks are held until the transaction completes, so no second writer can ever get in. This is the answer to February 2025’s third sub-question, and note that it is an argument about abort correctness, not about serializability — plain 2PL already serializes.

2026-01-22-q22026Q02Schedule classificationhard12 pts
Consider the schedule $$S = r_1(y)\; w_2(x)\; r_1(x)\; r_1(z)\; w_1(x)\; r_2(z)\; r_3(x)\; w_1(z)\; w_3(y)$$ a) (10 pts) Classify S with respect to VSR (listing view-equivalent schedules if any), CSR, 2PL, Strict 2PL, and TS Multi (Snapshot-Isolation conventions). Justify exhaustively, use class inclusion wherever possible, and show the 2PL and TS Multi results using tables. b) (2 pts) Suppose T2 aborts immediately after r2(z). Explain why this abort may compromise isolation, and which concurrency-control mechanism prevents it.

04 · SQL

Predicate locks and the SQL isolation levels

Phantom inserts need locks on rows that do not exist yet; and SQL exposes the whole spectrum of lock-duration choices as four named levels.

A phantom insert happens when a transaction adds items to a set another transaction has already read. No data lock can prevent it, because the row is not there to lock. A predicate lock extends locking to future data: for update Tab set B=1 where A>1, the lock is on the predicate A > 1, and no other transaction may insert, delete or update any tuple satisfying it. Implementations use index structures (gap locks); where predicate locks are unsupported, the lock degenerates to the whole table.

SQL:1999 defines four isolation levels, specifying which anomalies must be prevented. Note what they do not touch: a transaction always takes an exclusive lock on anything it modifies and holds it to completion — strict 2PL on writes — regardless of level. The levels only vary the protection of reads.

LevelDirty readNonrepeatable readPhantoms
READ UNCOMMITTEDpossiblepossiblepossible
READ COMMITTEDpreventedpossiblepossible
REPEATABLE READpreventedpreventedinsert only
SERIALIZABLEpreventedpreventedprevented

And the same table read as lock durations — the version worth memorising, because it explains why:

LevelRead locksWrite locks
READ UNCOMMITTEDnot required (ignores others’ locks too)long duration
READ COMMITTEDshort duration, data and predicatelong duration
REPEATABLE READlong duration on data, short on predicatelong duration
SERIALIZABLElong duration on data and predicatelong duration

Read down the read-lock column and the anomaly table falls out: no read locks admits dirty reads; short read locks mean a value can change between two reads; long data locks fix that but leave predicates unguarded, so inserts still slip in; long predicate locks close the last gap.

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET TRANSACTION READ ONLY;
×

SERIALIZABLE does not mean serial

Transactions at SERIALIZABLE do not execute one at a time. The requirement is only that they may commit if the result would be as if they had executed serially in some order. The locking needed to guarantee that frequently produces deadlocks, one of the transactions gets rolled back, and for that reason SERIALIZABLE is used sparingly and is not the default in most commercial systems. Note also that the standard states minimum requirements: MySQL and PostgreSQL both prevent phantom inserts at REPEATABLE READ, exceeding the standard.

05 · Deadlock

Deadlock: timeout, prevention, detection

Waiting is the price of locking, and waiting can become circular.

Transactions requesting a held lock are suspended and queued. Two pathologies follow:

  • Deadlock — two or more transactions in endless mutual wait, each holding what another needs.
  • Starvation — a single transaction waiting forever, typically a writer behind an endless stream of readers on a hot object such as an index root.

The canonical deadlock takes four operations:

T1: r1(x) w1(y)          S: r_lock1(x), r_lock2(y), r1(x), r2(y),
T2: r2(y) w2(x)              w_lock1(y) → wait,  w_lock2(x) → wait

Two representations: the lock graph is bipartite, with resources and transactions as nodes and arcs for requests and assignments; the wait-for graph keeps only transactions, with an arc TiTjT_i \to T_j when TiT_i waits for something TjT_j holds. A deadlock is a cycle in the wait-for graph.

Three families of resolution:

1 · Timeout

Kill and restart a transaction after a fixed wait. Simplest, widely used historically. The whole difficulty is choosing the value: too long wastes time on real deadlocks, too short kills transactions that were merely slow.

2 · Prevention

Kill transactions that could deadlock. Resource-based: request everything at once, or request resources in a global order — hard, because transactions rarely know their requests in advance. Transaction-based: use ids as ages (see below). Problem: many needless kills, since waiting is far more likely than deadlock.

3 · Detection

Kill only transactions that are deadlocked, by finding cycles in the wait-for graph. Needs a real algorithm, especially when resources are distributed — Obermarck’s, next section.

The two transaction-based prevention schemes are exam vocabulary. Both compare the age of the requesting transaction (RT) with that of the conflicting holder (CT), where a lower timestamp means older, and in both the oldest transaction always survives:

SchemeRT older than CTRT younger than CTStyle
Wait-dieRT waitsRT diesnon-preemptive
Wound-waitRT wounds CTRT waitspreemptive

A killed transaction restarts with its original timestamp, so it becomes progressively older and cannot starve.

How likely is any of this? With nn records and transactions making two accesses each under a uniform distribution, conflict probability is O(1/n)O(1/n) and deadlock probability O(1/n2)O(1/n^2) — much rarer, but real: the lectures quote once a minute in a mid-sized bank. The probability is linear in the number of transactions and quadratic in their length, which is the practical argument for keeping transactions short.

06 · Deadlock

Obermarck’s distributed detection

When a transaction spans nodes, no single node can see the whole wait-for graph. Obermarck’s algorithm detects the cycle anyway, by having each node forward summaries of what it knows.

The setting: transactions run on one main node but may spawn sub-transactions elsewhere, suspending until the sub-transaction returns. So there are two kinds of waiting — TiT_i waits for TjT_j on the same node because of a data lock, and a sub-transaction of TiT_i waits for another sub-transaction of TiT_i on a different node, via an external call.

Each node records its local situation as strings of the form

E_B → T2 → T1 → E_C

read as: a remote transaction at node B is waited for by T2T_2, which waits for T1T_1 (a local data lock), which in turn waits for something at node C. The EXE_X markers are external-call nodes.

The forwarding rule decides who tells whom, and it exists to stop every node from independently detecting the same deadlock:

Obermarck's forwarding rule

Node A sends its string to node B only when A contains a transaction TiT_i that is waited for by a remote transaction and that waits for a transaction TjT_j active on B — and i>ji > j.



Mnemonically: I send my information to you if a distributed transaction listed at me waits for a distributed transaction listed at you with a smaller index.

The algorithm runs periodically at each node, in four steps: receive graph information from the preceding nodes; merge it into the local graph; check for cycles among transactions and, if one is found, select a transaction in it and kill it; send updated information onward, propagating news of any kill.

Q

Obermarck has opened the February session every year since 2024

Distributed deadlock detection appears in 3 of the 14 papers — 2024-02-12, 2025-02-05 and 2026-02-13 — and those are every February session in the bank, three for three. It appears in no other month. The ask is always the same: run the algorithm under both conventions (i>ji > j and i<ji < j) and report every message sent at every iteration. February 2026 adds the question of whether the convention changes the number of steps; it does — 2 iterations versus 4 on the same input, detecting different cycles of the same global deadlock. Both are correct.

2026-02-13-q22026Q02Distributed deadlock detectionhard11 pts
Nodes A, B, C, D of a distributed transactional system know these waiting conditions: - A: $E_D \to T_4 \to T_3 \to E_D$; $E_B \to T_5 \to E_D$; $E_D \to T_2 \to E_C$ - B: $E_C \to T_5 \to E_A$; $E_C \to T_8$ - C: $E_D \to T_8 \to T_5 \to E_B$; $E_A \to T_2 \to T_3 \to T_8 \to E_B$ - D: $E_A \to T_5 \to T_4 \to E_A$; $E_A \to T_3 \to T_2 \to E_A$; $T_8 \to E_C$ 1. (7 pts) Execute Obermarck's algorithm sending messages $E_X \to T_i \to T_j \to E_Y$ (toward node Y) only if $i < j$. Report the messages of every iteration (no graphs needed). 2. (4 pts) Can a different convention (send only if $i > j$) terminate in a different number of steps? Show it on this example.
×

Three ways the message trace goes wrong

Comparing the wrong indices. The test uses the first and last transaction of the node’s distributed string; purely local transactions never enter the comparison.


Forgetting to re-evaluate after a merge. A node that could not transmit initially very often can once it has received a string — that is how the chain propagates.


Reporting the whole path. The message carries the summarized first-to-last dependency, not every intermediate hop.

07 · Refinements

The update lock

One deadlock pattern is far more common than all others, and one extra lock mode removes it.

The pattern is two transactions that both read an object and then both decide to write it:

r1(x) r2(x) w1(x) w2(x)

with SL/XL only:  SL1 granted, SL2 granted, XL1 → T1 waits, XL2 → T2 waits.   Deadlock.

Each holds a shared lock the other’s upgrade is waiting on. The update lock (UL) is requested by a transaction that intends to read and later write. Its compatibility is asymmetric — it tolerates existing readers, but no second UL:

RequestfreeSLULXL
SLOKOKOKNo
ULOKOKNoNo
XLOKNoNoNo

Replay the pattern with it: UL1 granted, UL2 waits immediately, XL1 granted — T1 finishes and T2 proceeds. No deadlock, and the cost is only that T2 waited a little earlier than it strictly had to. Update locks are requested through SELECT … FOR UPDATE:

START TRANSACTION;
SELECT * FROM t WHERE i = 2 FOR UPDATE;             -- rows write-locked until commit
SELECT * FROM t WHERE i = 2 FOR UPDATE NOWAIT;      -- error instead of waiting
SELECT * FROM t FOR UPDATE SKIP LOCKED;             -- skip rows locked by others

NOWAIT executes a command only if it does not run into a conflict; SKIP LOCKED returns only the unclaimed rows — the standard trick for queue tables, where avoiding conflict matters more than seeing every row.

08 · Refinements

Hierarchical (intention) locking

Update locks prudently extend the interval a resource is locked. Hierarchical locking answers the other question: locking what?

Locking a whole table destroys concurrency; locking single tuples means an enormous number of locks and slow conflict detection. So resources form a hierarchy — schema, table, fragment, page, tuple, field — and the objectives are to lock the minimum amount of data while recognising conflicts as early as possible.

The protocol has two halves:

  • Requests go top-down, from the root toward the granule you actually want.
  • Releases go bottom-up.

Beyond SL and XL, three modes declare intent at the coarser levels:

  • ISL — intention to lock a sub-element in shared mode.
  • IXL — intention to lock a sub-element in exclusive mode.
  • SIXL — lock this element in shared mode and intend to lock a sub-element exclusively (SL + IXL).

The rules for descending: to request SL or ISL on a non-root element you must already hold ISL or IXL on its parent; to request IXL, XL or SIXL you must already hold SIXL or IXL on the parent. And the granting table:

RequestfreeISLIXLSLSIXLXL
ISLOKOKOKOKOKNo
IXLOKOKOKNoNoNo
SLOKOKNoOKNoNo
SIXLOKOKNoNoNoNo
XLOKNoNoNoNoNo

Reading it pays off: ISL conflicts only with XL, so many readers can descend in parallel; XL conflicts with everything, so an exclusive lock high in the tree blocks all traffic beneath it.

Worked exam Filling the state table (exam 2026-07-02)

The setup

Hierarchy X(Y(A,B), Z(S,T)). Arrival sequence r1(S) w1(A) w2(Z) r2(A) r3(X) w1(Y) w3(X) w1(B). Locks are released after each transaction commits, which happens immediately after its last operation. A waiting transaction does not proceed with its later operations.

r1(S) — descend for a read

T1 takes ISL on X (root), ISL on Z, then SL on S. Top-down, one level at a time.

w1(A) — descend for a write

T1 needs exclusive access below Y, so it upgrades its ISL on X to IXL, takes IXL on Y, then XL on A.

w2(Z) — the first conflict

T2 takes IXL on X (compatible with T1’s IXL), then requests XL on Z — which T1 holds ISL on. The table says No. T2 waits, and therefore its next operation r2(A) is suspended too.

r3(X) — the second conflict

T3 requests SL on the root. Two IXLs are held there, and SL versus IXL is No. T3 waits, suspending its w3(X) as well.

w1(Y), w1(B) — T1 finishes

T1 upgrades IXL on Y to XL; B sits beneath a node it now holds exclusively. T1 commits and releases everything bottom-up.

The queue drains

T2’s XL on Z is now granted, r2(A) proceeds (ISL on Y, SL on A), T2 commits. Then T3’s SL on X is granted, upgraded to XL for w3(X), and T3 commits. The post-commit replay is part of the answer — an answer that stops at the deadlock-free conflict is incomplete.

Q

Hierarchical locking is rising

It is absent from every paper before mid-2025, then appears in 2025-06-04 and 2026-07-02 — two of the last five sessions, both summer sittings. The deliverable is a

step-by-step table with one row per operation or commit and one column per resource

, showing every lock state including the waits. Given the trend, treat it as likely rather than exotic.

2026-07-02-q12026Q01Hierarchical lockinghard10 pts
Given the resource hierarchy X(Y(A,B), Z(S,T)), describe the behaviour of the arrival sequence $$r_1(S)\; w_1(A)\; w_2(Z)\; r_2(A)\; r_3(X)\; w_1(Y)\; w_3(X)\; w_1(B)$$ under a scheduler applying hierarchical locking. Locks are released after each transaction's commit, which occurs immediately after its last operation. Show the step-by-step lock-state table for every resource.

In hierarchy X(Y(A,B)), transaction T1 holds ISL on X and SL on Y. T2 now wants to write tuple A. What happens?

Load-bearing ideas

  • Locks alone are not enough. Obeying the conflict table still permits nonrepeatable reads; the missing ingredient is when you may release.
  • Two-phase rule: no lock acquired after any lock released. 2PL ⊂ CSR ⊂ VSR, strictly — the counter-example r1(x) w1(x) r2(x) w2(x) r3(y) w1(y) is CSR but not 2PL.
  • Strict 2PL holds locks to commit/rollback, which is what blocks dirty reads — and long-duration write locks are mandatory at every isolation level, because otherwise abort processing itself becomes impossible (dirty write).
  • Four isolation levels = four read-lock durations. No read locks → dirty reads; short → nonrepeatable; long on data → phantom inserts remain; long on data + predicate → serializable.
  • Deadlock = a cycle in the wait-for graph. Timeout, prevention (wait-die non-preemptive, wound-wait preemptive — the older transaction always survives, restart keeps the timestamp), or detection. Deadlock probability is quadratic in transaction length: keep transactions short.
  • Obermarck forwards a node’s summarized string when i>ji > j (or i<ji < j — both conventions work, in different numbers of steps), merges on receipt, and looks for a cycle.
  • Update lock removes the r1 r2 w1 w2 upgrade deadlock; SELECT … FOR UPDATE.
  • Hierarchical locking: request top-down, release bottom-up, with ISL/IXL/SIXL declaring intent.
  • Exam radar. Re-derive before the exam: the 2PL refutation (name the release and the later acquisition); the Strict 2PL refutation (a transaction forced to release before its own commit point); the Obermarck message trace under both conventions; and the hierarchical state table including the post-commit replay.