Internals · MVCCscheduleinterleavingserializabilityconflict serializabilityprecedence graph

Concurrency Control: Schedules and Serializability

Interleave four operations from two transactions and €20 disappears; the engine's job is to allow only interleavings whose result equals some serial order, and the two ways to do that — refuse conflicting steps (locking) or keep every version and check afterwards (multi-version / optimistic) — are the roots of every isolation mechanism.

▶ InteractiveInterview question
Progress

Why this exists

The mechanism as the answer to a problem — read this before the name.

  1. Problem

    Two sessions run at once: A reads balance 100 and writes 80; B reads 100 and writes 70. Run one after the other the result is 50. Interleaved as read, read, write, write the result is 70 and A's €20 debit is gone — with no error anywhere.

  2. Naive solution

    Run transactions one at a time. A global lock at BEGIN, released at COMMIT. Every result is trivially correct.

  3. Why it breaks

    One slow report freezes every checkout. A commit that waits on an fsync holds the whole database for milliseconds while thousands of cores idle. Throughput equals the throughput of one session.

  4. Better idea

    Most interleavings are harmless — two transactions touching different rows can overlap freely. Only conflicting operations (same item, at least one a write) need ordering. Allow any interleaving whose conflicts all point the same way as some serial order.

  5. Internal mechanism

    Either block a conflicting step until the other transaction finishes (two-phase locking), or let both proceed on separate versions and detect at commit whether the conflicts still form a consistent order (multi-version, optimistic). Both enforce conflict-serializability; they differ in when they pay.

  6. Trade-offs

    Locking pays in waiting and deadlocks and is exact. Optimistic pays in aborted work and retries and is cheap when conflicts are rare. Neither is free, which is why production engines combine them.

  7. Real database

    PostgreSQL: MVCC for reads, row locks for writes, dependency tracking (SSI) for SERIALIZABLE. InnoDB: MVCC for consistent reads, two-phase locking with next-key locks for everything that writes.

Choose your depth

The same mechanism at four altitudes. Start where you are; come back deeper.

The interleaving problem

Two correct programs run at the same time can produce a wrong answer, because each read a value that the other was about to change. Serial execution is always correct; concurrent execution is only correct when it is *equivalent* to a serial one. Concurrency control is the set of rules that admits the equivalent interleavings and rejects or delays the rest.

What happened?

Account 7 holds 100. Transaction A wants to debit 20, transaction B wants to debit 30. Each does the obvious thing: read the balance, subtract, write the result. The engine runs them interleaved.

Final balance: 70. Correct balance: 50. Nothing failed, nothing was rejected, no lock was requested. A's write was simply overwritten by a value computed from a stale read. This is the lost update, and every concurrency control mechanism exists to make this schedule either impossible or detectable.

The interleaving. Time runs downward.
time   TX A                       TX B                     balance on disk
 1     READ balance  -> 100                                   100
 2                                READ balance  -> 100        100
 3     WRITE balance := 80                                     80
 4                                WRITE balance := 70          70   <- A's -20 is gone
 5     COMMIT
 6                                COMMIT

serial A then B:  100 -> 80 -> 50        serial B then A:  100 -> 70 -> 50
this schedule:    70   (equivalent to neither)

Schedules and conflicts

A schedule is the global order in which the operations of concurrent transactions actually executed. A serial schedule runs each transaction to completion before the next starts; serial schedules are correct by definition, whatever the transactions do. Two operations conflict when they come from different transactions, touch the same item, and at least one is a write: read–write, write–read and write–write pairs. Read–read pairs never conflict.

Swapping two adjacent non-conflicting operations cannot change any result, since neither could observe the other. A schedule is conflict-serializable if a sequence of such swaps turns it into a serial schedule. The test is mechanical: draw a node per transaction and an edge Ti → Tj for every conflict where Ti's operation came first. If the graph is acyclic, a topological order of it is a serial schedule the interleaving is equivalent to. If it has a cycle, no serial order matches. In the schedule above, B's read precedes A's write (B → A) and A's write precedes B's write (A → B): a cycle of length two.

Precedence graph of the lost-update schedule
conflicts:
  2. B: READ(bal)   before   3. A: WRITE(bal)   ->  edge B -> A   (rw)
  1. A: READ(bal)   before   4. B: WRITE(bal)   ->  edge A -> B   (rw)
  3. A: WRITE(bal)  before   4. B: WRITE(bal)   ->  edge A -> B   (ww)

      A  <-----  B
      A  ----->  B          cycle: not conflict-serializable

The anomalies as schedules

Every anomaly in the practical lesson is a particular shape of conflicting schedule. Seeing them as schedules explains why they need different mechanisms: some are visible on a single item, and some only exist at the level of predicates or of pairs of items.

Five anomalies, written as operation sequences (r = read, w = write, c = commit, a = abort)
lost update          rA(x) rB(x) wA(x) wB(x) cA cB        two writers, both from a stale read
dirty read           wA(x) rB(x) aA  cB                    B used a value that never committed
non-repeatable read  rA(x) wB(x) cB  rA(x) cA              same row, two answers inside A
phantom              rA(P) wB(y in P) cB  rA(P) cA         P is a predicate; y did not exist to lock
write skew           rA(x) rA(y) rB(x) rB(y) wA(x) wB(y) cA cB
                     each read both, each wrote a different one; no ww conflict at all

Family one: pessimistic (locking)

Prevent the conflict from forming. Before reading, take a shared lock on the item; before writing, an exclusive one; a conflicting request waits. Two-phase locking (2PL) adds the rule that makes this correct: a transaction acquires all its locks before it releases any (a growing phase, then a shrinking phase). Under 2PL every admitted schedule is conflict-serializable, because the point where a transaction holds its maximum set of locks orders it with respect to every transaction it conflicts with. Strict 2PL holds locks until commit, which additionally rules out dirty reads and cascading aborts, and is what real engines do.

In the lost-update schedule, A's read would take a shared lock; B's read a second shared lock; A's write would need to upgrade to exclusive and would wait for B; B's write would wait for A — a deadlock, which is detected and broken. The anomaly turns into an abort, which is the correct outcome. Locking is exact, and it pays in waiting and deadlocks; The Lock Manager shows the structure that grants the locks and Deadlock Detection: The Waits-For Graph the one that breaks the cycles.

Family two: optimistic and multi-version

Optimistic concurrency control (Kung and Robinson, 1981) assumes conflicts are rare. A transaction runs in three phases: *read* — execute freely against the database, buffering writes privately and recording the read set; *validate* — at commit, check that no transaction that committed since this one started wrote anything in its read set; *write* — install the buffered writes. Fail validation and the transaction restarts. No waiting ever, no deadlocks ever; the cost is wasted work under contention.

Multi-version concurrency control takes the reader side of that idea and makes it structural: never overwrite, keep every version, and give each transaction a snapshot that selects the versions it may see. Readers need no locks and no validation, because the versions they read cannot change. Writers still conflict, and engines handle that either with row locks (a write waits for a concurrent writer of the same row and then re-checks, or aborts) or with validation at commit. The result is a hybrid: pessimistic for write–write conflicts, optimistic for everything else. MVCC Internals: Version Chains and Snapshots derives the version chain and the visibility rule.

The two families side by side
Pessimistic (2PL)Optimistic / multi-version
When conflicts are handledbefore the operation (wait)at commit (validate) or never for readers
Readers block writersyesno
Writers block readersyesno
Deadlockspossibleimpossible (pure OCC); write locks reintroduce them
Wasted worknone; time is spent waitingaborted transactions are redone
Best fithigh contention, expensive retrieslow contention, read-heavy

Why engines mix them

A workload is typically nine reads for every write. Giving reads the multi-version path removes them from the lock manager entirely: no lock table entries, no waiting, no deadlock participation. Writes are rarer and benefit from the exactness of locks — a row lock held to commit means a write–write conflict resolves in a definite order and the loser sees the winner's result rather than restarting blindly. Full serializability, which needs read–write conflicts tracked too, is the rare requirement, so it gets a separate mechanism that only those transactions pay for.

PostgreSQL: snapshot reads, row locks for writers, SSI dependency tracking at SERIALIZABLE. InnoDB: snapshot (read view) reads, next-key locks for writers and locking reads, SELECTs promoted to locking reads at SERIALIZABLE. Oracle: snapshot reads, row locks, and a SERIALIZABLE that is really snapshot isolation. The names of the levels are the same; the mechanisms — and therefore the anomalies that slip through — are not, which is the subject of Isolation Levels: The Mechanism Behind Each.

Key points

  • A schedule is the interleaving that actually ran. It is correct when equivalent to some serial order.
  • Only conflicts (same item, different transactions, at least one write) matter; conflict-serializable means the precedence graph is acyclic.
  • Every named anomaly is a schedule shape: lost update and non-repeatable read on one item, phantom on a predicate, write skew on two items with no write–write conflict.
  • Two-phase locking prevents bad schedules by waiting; optimistic and multi-version control admit them and validate, or keep versions so readers never conflict.
  • Real engines run MVCC for reads and locks for writes, adding dependency tracking or locking reads only for SERIALIZABLE.

Interleave two transactions

Interleave two transactions
Click operations into TX A or TX B to build a schedule. The evaluator runs it with no isolation at all (every read sees the latest write) and names the anomaly the interleaving exhibits — or proves it serializable.
TX A — add operation
TX B — add operation
accounts: id=1 balance=100
tTX ATX B
1
SELECT balance
-- 100
2
SELECT balance
-- 100
3
UPDATE balance = 100 − 20 = 80
4
COMMIT
5
UPDATE balance = 100 − 30 = 70
6
COMMIT
Verdict
Lost update. B computed its new value from balance = 100, which it read earlier, but balance had since become 80 through A's committed write. B's write silently overwrites it: A's update is lost. Neither side saw an error. Fix: do the arithmetic in one statement (SET balance = balance − 20), lock at read time (FOR UPDATE), or use Repeatable Read, where the second writer gets 40001.
Final state
balance = 70 (should be 50)
Precedence graph (committed transactions)
A → B B → A — cycle

A schedule is conflict-serializable iff its precedence graph is acyclic — the same cycle detection you run on any directed graph. With only two nodes a cycle means both edges exist; the deadlock detector two lessons on uses the same test on a waits-for graph.

Educational simulation — the evaluator deliberately applies no locks and no snapshots, so you can see what isolation has to prevent.

When to use — and when not

Use it when
  • Pessimistic control fits when conflicts are frequent and a retry is expensive or user-visible: balances, inventory, seat assignment.
  • Optimistic and multi-version control fit read-heavy workloads where a reader must never wait for a writer and conflicts are rare.
Avoid it when
  • Pure optimistic control does not fit hot rows under heavy contention — abort rates climb until nothing commits.
  • Pure locking does not fit workloads dominated by long reads: every report would block every writer.

Failure modes

  • Reasoning about correctness per statement instead of per schedule — the statements were fine; the interleaving was not.
  • Expecting snapshot isolation to reject write skew: there is no write–write conflict for it to see.
  • Choosing optimistic control for a hot counter and discovering the retry loop is the workload.

Where you meet this

Back up to the practical layer, and across to the rest of Engineer Atlas.

Cross-domain bridges