skip to content

What is a cascading abort in a database, how can releasing exclusive (write) locks before commit under basic two-phase locking cause one, and how does holding those locks until commit eliminate the problem?

level: seniorimportance: should knowfreq 38%

answer

  1. Dirty read → abort propagates down the read-from graph
  2. Recoverable ⊃ cascadeless ⊃ strict
  3. Commit-before-abort = unrecoverable, no fix exists
  4. Before-image undo would erase a later writer
  5. Write locks held to commit at EVERY isolation level

basics

~20 s

A cascading abort is one transaction's rollback forcing others to roll back because they read its uncommitted writes. Releasing write locks early lets that dirty read happen. Holding exclusive locks to commit means nobody ever reads uncommitted data, so no cascade is possible.

solid answer

~50 s

Under basic 2PL a transaction may drop its write lock as soon as it stops acquiring — before commit. T2 can then acquire that lock and read T1's uncommitted value. If T1 aborts, T2's work is based on data that never existed, so T2 must abort too; anything that read T2's writes must abort as well. That fan-out is a **cascading abort**, and its cost is unbounded and unpredictable. Worse, if T2 **commits** before T1 aborts, the schedule is **non-recoverable** — you cannot un-commit T2. Rollback also becomes unsound: undoing T1 by restoring its before-image would silently erase T2's later write to the same row. **Strict 2PL** — holding all exclusive locks until commit or abort — makes schedules *cascadeless*: transactions only ever read committed data. Rollback then reduces to restoring before-images, which is why real engines hold write locks to commit at every isolation level, including READ COMMITTED.

code

text · 12 lines
text
(a) cascade
T1: X-lock(A) w(A=5) unlock(A) .................. abort
T2:                    S-lock(A) r(A=5) w(B) ..... must abort
T3:                                      r(B) .... must abort

(b) non-recoverable
T1: w(A=5) unlock(A) ......................... abort
T2:          r(A=5) ... COMMIT   <- already durable, cannot be undone

(c) rollback destroys a later write   (A starts at 1)
T1: w(A=5) unlock(A) ................ abort -> restores A=1
T2:          X-lock(A) w(A=9) COMMIT        <- A=9 silently lost

go deeper

for a junior

Define a cascading abort and say that holding write locks until commit prevents transactions from reading uncommitted data.

for a middle

Sketch the schedule, name the recoverable / cascadeless / strict hierarchy, and connect strict 2PL to the strict class.

for a senior

Lead with the operational consequences: unbounded abort fan-out, the impossible non-recoverable commit, and before-image undo destroying a later write — and explain why write-lock strictness is not part of the isolation dial.

for a principal

Contrast enforcement strategies — strictness by locking versus by versioning in MVCC — and reason about the cost of holding write locks across a durable commit in latency-sensitive or distributed systems.

## The three recoverability classes Serializability is about *isolation*; it says nothing about what happens when a transaction fails. A schedule can be perfectly conflict-serializable and still be impossible to recover from. The classic hierarchy, from weakest to strongest: - **Recoverable** — a transaction commits only *after* every transaction whose writes it read has committed. Guarantees you never have to un-commit something. - **Cascadeless (avoids cascading aborts, ACA)** — transactions only ever *read* data written by committed transactions. Stronger: no dirty reads at all, so no abort ever propagates. - **Strict** — transactions neither read nor **overwrite** an item written by an uncommitted transaction. Strongest of the three, and the one that makes before-image undo correct. Each implies the one above it. Strict 2PL delivers the strict class, which is where the name comes from. ## How basic 2PL produces a cascade Basic 2PL only requires acquire-before-release ordering; it lets a transaction drop a write lock in its shrinking phase, well before commit. Consider: ``` T1: lock-X(A) write A=5 unlock(A) ... more work ... ABORT T2: lock-S(A) read A=5 write B=A*2 ... T3: read B ... ``` T2 read a value that, after T1's rollback, never existed in any committed state. T2's computation is invalid, so T2 must abort. T3 read T2's output, so T3 must abort. In a real workload this fan-out follows the data-dependency graph and is bounded only by how many transactions touched the row and its descendants — a single failure, possibly a deadlock victim chosen at random, can wipe out a large amount of committed-looking work. The engine also has to *track* the read-from relationships to know whom to cascade to, which is bookkeeping nobody wants. ## The two harder problems underneath Cascades are the visible symptom; two deeper issues make early write-lock release untenable. **Non-recoverability.** Change the schedule so T2 commits before T1 aborts. Now the system is stuck: it has already durably promised T2's result to the client, and that result was derived from a write that is being undone. There is no correct action available. Recoverable schedules exist precisely to make this state unreachable. **Unsound rollback.** Engines undo a transaction by restoring the **before-image** of each row it changed (the physiological undo you find in ARIES-style recovery). Suppose A starts at 1, T1 writes A=5, releases the lock, and T2 then writes A=9. When T1 aborts, restoring T1's before-image sets A back to 1 — silently destroying T2's committed write. This is the *lost update on rollback* problem, and it is why the **strict** class forbids not just reading but **overwriting** uncommitted data. Avoiding it without strictness would require version-aware undo, i.e. rebuilding a good part of MVCC. ## What holding exclusive locks to commit buys Strict 2PL says: keep every exclusive lock until commit or abort. Because a write lock conflicts with both shared and exclusive requests, no other transaction can read or overwrite the row while the writer is in flight. Therefore: - Every value any transaction reads was written by a committed transaction → **cascadeless**, so aborts never propagate and no read-from tracking is needed. - No one can overwrite your uncommitted row → **before-image undo is always correct**, so rollback is a local, bounded operation. - Commit ordering is safe → **recoverable**. The cost is lock hold time: the write lock now lives from first modification through commit, including the fsync of the commit record. That is why long transactions and slow commits hurt a lock-based engine so much — the tail of the commit path is *inside* the critical section. ## Why this is not an isolation-level knob The practically important consequence: engines keep write locks to commit even at READ COMMITTED and READ UNCOMMITTED. Weakening isolation relaxes the **read** side (how long shared locks are held, or whether they are taken at all); the write side stays strict because relaxing it would break rollback and recovery rather than merely permitting an anomaly. If a candidate says "at READ UNCOMMITTED nothing is locked", that is the misconception — dirty *reads* are allowed, dirty *writes* are not, at any level. MVCC engines reach the same guarantee by different means: a writer creates a new row version that is invisible to everyone until its transaction commits, so readers automatically see only committed data and abort never cascades — the write side is still effectively locked (row-level write locks / first-updater-wins), while readers are exempted because they never need the lock at all. ## Answering well Define the cascade, show the two-line schedule that produces it, then escalate to the two things that actually force implementations' hands — non-recoverable commits and before-image undo destroying a later write — and finish with: this is why write locks are held to commit at every isolation level, and why "strict" in strict 2PL refers to the strict schedule class.

  • Does an MVCC engine suffer cascading aborts?
    No. A writer produces a new row version tagged with its transaction id, and that version is invisible to other transactions until it commits, so nobody can read uncommitted data in the first place. Writers still conflict with writers — via row-level write locks or first-updater-wins — so the strict-schedule property on the write side is preserved by versioning rather than by holding shared locks out.
  • If cascading aborts are so damaging, why does the theory bother defining plain 2PL at all?
    Because the two-phase rule alone is what proves conflict-serializability, and separating that proof from recoverability keeps the concepts clean: isolation and failure-atomicity are independent properties. Basic 2PL is the minimal protocol for the isolation half; strictness is then added explicitly for the recovery half. Real engines implement the combination, but the split is why you can reason about isolation levels and rollback correctness separately.

saying these in an interview costs you the question

  • Claiming a cascading abort is the same thing as a deadlock
  • Believing conflict-serializable implies recoverable
  • Saying READ UNCOMMITTED means no locks are taken at all
  • Thinking undo by before-image is always safe regardless of locking discipline
  • Assuming cascades affect at most one other transaction

context