CMD Guide
HomeDatabasesTransactions & Concurrency Control

Transactions & Concurrency — Recovery (REDO/UNDO), MVCC Internals, Serializability & Locking (Deep Dive)

The Transactions pages already give you the isolation-level table — which anomaly each level prevents. This page derives the machinery underneath that table: why a crash recovery needs both REDO and UNDO (not just one), what a "snapshot" actually is as a data structure, why two engines that both call themselves "serializable" behave completely differently under load, and why a lock taken on one row can silently become a lock on the whole table.

1. Recovery — why REDO and UNDO both exist

Mechanism: recovery machinery is not a design choice made for its own sake — it is forced by the buffer manager's policy on two independent questions. STEAL: may a dirty page (holding an uncommitted transaction's write) be evicted from the buffer pool and written to disk before that transaction commits? FORCE: must every page a transaction touched be flushed to disk at the moment it commits, before the commit can return?

A real engine answers STEAL = yes, FORCE = no, because the alternative is unusable: forbidding STEAL means the buffer pool can never evict a page that belongs to a still-running transaction, which starves memory under any long transaction; requiring FORCE means every commit blocks on random-access disk I/O for every page touched, which is ruinously slow compared to one sequential log write. STEAL+NO-FORCE is fast for exactly the reason the other three combinations are not — but it is precisely what forces both halves of recovery to exist:

Take away either half of STEAL+NO-FORCE and one recovery phase becomes unnecessary (see the diagram) — but you pay for it with a slower, less flexible buffer manager. This is the ARIES rationale in one sentence: STEAL+NO-FORCE buys write throughput at the cost of needing both REDO and UNDO on restart.

2x2 matrix of STEAL vs NO-STEAL and FORCE vs NO-FORCE buffer policies; only STEAL plus NO-FORCE, which real engines use, requires both REDO and UNDO recovery
2x2 matrix of STEAL vs NO-STEAL and FORCE vs NO-FORCE buffer policies; only STEAL plus NO-FORCE, which real engines use, requires both REDO and UNDO recovery

Traced example: STEAL+NO-FORCE crash recovery, second by second

Three transactions run against pages P_A, P_B, P_C (each holding one row). The write-ahead log (WAL) is fsync'd at every commit — that is the durability mechanism: the log record is guaranteed durable the instant COMMIT returns, regardless of whether the data page itself ever reached disk before the crash.

LSNEventBuffer pool / disk effect
10–12T1: BEGIN; write A: 0→10; COMMIT (WAL fsync)P_A dirty in buffer pool, not yet flushed to disk (NO-FORCE)
13–14T2: BEGIN; write B: 0→20 (never commits)Buffer pool under pressure evicts and flushes P_B to disk — an uncommitted page hits disk (STEAL)
15–17T3: BEGIN; write C: 0→30; COMMIT (WAL fsync)P_C dirty in buffer pool, not yet flushed (NO-FORCE)
CRASH. On-disk state at this instant: P_A=0 (committed, but not flushed), P_B=20 (uncommitted, but flushed), P_C=0 (committed, but not flushed).

Recovery (ARIES: Analysis → Redo → Undo):

  1. Analysis pass. Scan the WAL forward from the last checkpoint. T1 and T3 have COMMIT records — they are winners. T2 has no COMMIT record — it is a loser and must be rolled back.
  2. Redo pass. Replay every logged change in LSN order — T2's included — regardless of commit status ("repeating history"): apply LSN11 (A:0→10), LSN14 (B:0→20), LSN16 (C:0→30). Each page carries its own on-disk LSN, so a redo is skipped if the page's stored LSN is already ≥ the log record's LSN — this is what makes redo idempotent and safe to re-run after a second crash mid-recovery. Here, P_B's on-disk LSN is already 14 (it was flushed before the crash), so its redo is a no-op; P_A and P_C's redos actually apply. After this pass: P_A=10, P_B=20, P_C=30 — the exact state at the instant of the crash, winners and losers alike.
  3. Undo pass. Now roll back every loser. T2 is the only one: undo LSN14, writing a compensation log record (CLR) B:20→0 so the undo itself is durable and will not be re-undone if the recovery process crashes again.

Final state: P_A=10 (T1's commit survived, thanks to REDO), P_B=0 (T2's uncommitted write was rolled back, thanks to UNDO), P_C=30 (T3's commit survived, thanks to REDO). Both halves of recovery were required by the same two pages that got flushed at the "wrong" time — exactly the STEAL/NO-FORCE consequence above.

Commit durability mechanics beyond the single fsync

2. MVCC internals & engine differences

Mechanism: a snapshot is not a single timestamp or counter — it is a small data structure: (xmin horizon, xmax, xip_list). xmin is the oldest transaction still running anywhere when the snapshot was taken; xmax is the next transaction id not yet assigned; xip_list is the exact set of transaction ids that were in progress at that instant. A row version is visible to a snapshot only if its creator committed before the snapshot AND its creator is not in the snapshot's in-progress list — the xip_list is the piece people forget, and it is exactly what lets a transaction that started earlier but committed later stay invisible to a snapshot taken in between. (The full step-by-step trace of this — xmin/xmax on a row as it's updated by one writer and read by two readers — is worked in "MVCC & Locking — Snapshots, Row Locks & Deadlocks"; this page assumes that visibility rule and focuses on what differs across engines.)

Where the old versions live is the deep engine-level difference:

Same guarantee (a stable, consistent snapshot with no read locks), opposite storage trade-off: Postgres pays in table/index bloat that a background process must reclaim; InnoDB/Oracle pay in undo-segment growth pinned by whichever reader is oldest. Neither cost disappears — it just shows up in a different place, which is why "keep transactions short" is a universal MVCC rule, not a Postgres-specific one.

3. Serializable is not one thing: 2PL vs SSI

Mechanism: "serializable" only promises that the outcome is equivalent to some serial order of the transactions — it says nothing about how that guarantee is enforced, and the two dominant mechanisms behave completely differently under load.

The cost/behavior difference is the whole judgment call: 2PL pays in reduced concurrency while waiting (and occasional deadlocks); SSI pays in wasted work and commit-time aborts under contention (and mandatory retry logic). Both are truly serializable — they just move the cost to a different moment. (The write-skew anomaly that plain Snapshot Isolation lets through, and exactly why SSI is what closes that specific hole, is traced in full in "Write Skew, Lost Update & Snapshot Isolation" — this page assumes that anomaly and focuses on the 2PL-vs-SSI mechanism choice.)

Side by side: strict 2PL blocks the second writer until the first commits and can deadlock, while SSI lets both transactions run to commit without blocking and instead aborts one transaction when it detects a dangerous read-write dependency cycle
Side by side: strict 2PL blocks the second writer until the first commits and can deadlock, while SSI lets both transactions run to commit without blocking and instead aborts one transaction when it detects a dangerous read-write dependency cycle

4. Isolation anomalies, grounded in mechanism

Each classic anomaly is a specific gap between "what a lock/snapshot covers" and "what the transaction actually depends on":

Classic phantom reads vs serializability: engines close different gaps with different tools. Do not collapse "phantom prevention" into one mechanism and one isolation level — the answer depends on what you mean and which engine you are on.

Traced contrast (same two transactions, two engines):

StepT1T2PG RR (SI)PG SERIALIZABLE (SSI)InnoDB RR (gap locks)
1SELECT COUNT(*) … WHERE free → 1snapshot S1 sees 1 freesame + SIREAD on rangenext-key locks gaps in free range
2INSERT a matching free row; COMMITallowed; T1 still sees 1 on re-SELECTINSERT ok; rw-edge recordedT2 blocks on gap until T1 ends
3re-SELECT COUNTstill 1 (classic phantom read blocked by snapshot)still 1still 1 (and T2 may still be waiting)
4INSERT booking based on "only 1 left"; COMMIT(if T2 also booked from "only 1")both commits can succeed → write skew possibleone COMMIT aborts (serialization failure)T2 runs only after T1; serial order forced by locks

Takeaway: Postgres RR already kills classic phantom reads via the transaction snapshot; SSI is for serializability (write skew and related multi-object anomalies); InnoDB RR kills range phantoms by locking gaps. Interview answers that say "Postgres prevents phantoms with SSI like InnoDB uses gap locks" conflate three different mechanisms.

5. Lock taxonomy & the ACID subtlety

Shared (S) vs exclusive (X): any number of transactions may hold a shared lock on the same row concurrently (compatible with each other) — that's what lets many readers proceed together; an exclusive lock is incompatible with every other lock, shared or exclusive, on that row.

Intent locks (IS/IX) and escalation. Before a transaction takes a row-level S or X lock, it first takes an intent lock (IS or IX) at the table level, announcing "I hold (or intend to hold) row-level locks somewhere inside this table." This lets a different transaction that wants a whole-table lock check compatibility in O(1) against the intent lock, instead of scanning every row lock in the table. Under memory pressure — too many individual row locks consuming lock-table memory — the engine can perform lock escalation: replace many fine-grained row locks with one coarse table-level lock. This bounds memory, but the concurrency cost is real and immediate — every other transaction that only needed a different, unrelated row in that table now blocks on the whole table instead of just its row. Escalation trades memory safety for a concurrency cliff, which is why it shows up in production as a sudden, hard-to-diagnose throughput collapse under load rather than a gradual slowdown.

The ACID subtlety: not all four letters live at the same layer. Atomicity, Isolation, and Durability are engine properties — the WAL/REDO/UNDO machinery (§1), the locking/MVCC machinery (§2–4), and the fsync/checkpoint machinery all deliver them regardless of what the application does. Consistency is different — it is largely an application concern. The database enforces the constraints it knows about (foreign keys, unique indexes, check constraints), but whether a transaction moves the database from one business-valid state to another (the on-call invariant, the inventory-never-negative invariant) depends entirely on the application writing correct transaction logic against those constraints — the engine cannot invent an invariant it was never told about.

Distributed atomic commit, briefly. A single-node transaction's atomicity is exactly §1's REDO/UNDO story. Across multiple nodes, atomicity needs two-phase commit (2PC): a coordinator asks every participant to prepare (durably log its half of the change and vote), and only issues the actual commit once every participant has voted yes — guaranteeing all-or-nothing across nodes at the cost of blocking every participant while the coordinator decides. This interacts with replication lag the same way single-node durability does: a participant that acks "prepared" but whose change hasn't yet replicated to its own followers can lose that change in a failover, which is the same RPO-window problem covered in the replication-lag/failover material — 2PC's cross-node atomicity and a single node's replication durability are two separate guarantees that both have to hold.

Pitfalls

Judgment layer

2PL vs SSI. Choose lock-based 2PL-style serializability when contention is low-to-moderate and you cannot tolerate application-level retry logic (or the engine doesn't offer SSI, e.g. MySQL, SQL Server). Choose SSI (PostgreSQL SERIALIZABLE) when reads dominate and you want near-snapshot-isolation concurrency with true serializable correctness — but only if the application is written to catch a serialization-failure error and retry the whole transaction; without that retry loop, SSI's aborts surface as user-facing failures.

Isolation level choice. Default to Read Committed for ordinary CRUD where each statement's own consistency is enough (Postgres and Oracle default here; MySQL/InnoDB defaults to REPEATABLE READ). Move to Repeatable Read / Snapshot Isolation when a transaction must see one consistent view across multiple statements (a report, a multi-step calculation) — on Postgres RR this already freezes the visible row set (no classic phantom reads), but write skew remains. Reach for full Serializable only when a cross-row invariant is genuinely business-critical (double-spend, capacity limits, scheduling) — SSI closes write skew and related predicate-write races that SI leaves open, at the cost of serialization-failure retries.

Postgres heap-MVCC vs. undo-segment MVCC. Neither storage strategy is free of long-transaction pain — they just fail differently. Postgres's heap-resident versions mean a long transaction bloats the table and every index on it, recovered later by (auto)vacuum; InnoDB/Oracle's undo-segment versions mean a long transaction pins the undo/history list, growing it until that reader finishes, with no separate cleanup pass needed once it does. If your workload has occasional very-long read transactions (reporting, batch export), that is a reason to route them to a replica or a snapshot export rather than an argument for choosing one engine over the other — both need the same operational discipline: keep transactions short.

Takeaways

Related pages


Synthesized from the ARIES recovery paper (Mohan et al.) and CMU 15-445/645 (Andy Pavlo) on buffer management and logging; PostgreSQL documentation on MVCC, snapshots, and Serializable Snapshot Isolation; Cahill, Röhm & Fekete, "Serializable Isolation for Snapshot Databases" (SIGMOD 2008); the InnoDB manual on gap/next-key locking and multi-versioning; M. Kleppmann, Designing Data-Intensive Applications ch. 7 & 9. Cross-references: "MVCC & Locking — Snapshots, Row Locks & Deadlocks," "Write Skew, Lost Update & Snapshot Isolation," "Isolation Levels & Anomalies," "Databases for System Design — Isolation Anomalies, B-Tree vs LSM, Join Planning, Quorum & Failover," "Replication Lag & Failover," "2PC vs Saga vs TCC." Re-authored/Deepened for this guide.

🤖 Don't fully get this? Learn it with Claude

Stuck on Transactions & Concurrency — Recovery (REDO/UNDO), MVCC Internals, Serializability & Locking (Deep Dive)? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Transactions & Concurrency — Recovery (REDO/UNDO), MVCC Internals, Serializability & Locking (Deep Dive)** (Databases) and want to truly understand it. Explain Transactions & Concurrency — Recovery (REDO/UNDO), MVCC Internals, Serializability & Locking (Deep Dive) from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Transactions & Concurrency — Recovery (REDO/UNDO), MVCC Internals, Serializability & Locking (Deep Dive)** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Transactions & Concurrency — Recovery (REDO/UNDO), MVCC Internals, Serializability & Locking (Deep Dive)** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Transactions & Concurrency — Recovery (REDO/UNDO), MVCC Internals, Serializability & Locking (Deep Dive)** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes