A transaction is a multi-step database operation that must run as if it were a single atomic unit, even when thousands of other transactions are running on the same data simultaneously.
5 figures
motivation · Case Study: Taylor Swift Concert Sales on Ticketmaster
One Buy click is one transaction: three steps, all or none
One fan's Buy click is a single transaction: check the seat is open, charge the card, record the sale, bundled so either all three commit or none do, never a charge without a seat and never the same seat to two fans. Multiply by the on-sale crush, every fan racing for the same seats, and you have the problem this module solves.
Notes ↗motivation · Case Study: Taylor Swift Concert Sales on Ticketmaster
The Taylor Swift on-sale, by the numbers
The real failure was load, not correctness. About 12 million visitors and scalper bots drove 3.5 billion requests, four times the prior peak (violet). Under it, availability bent: queues froze thousands deep, Ticketmaster throttled and paused sales to stay up, and seat holds and payments timed out (red). But the transactions held: no seat sold twice, bots got nothing, and a record 2.4 million tickets sold in a day (green). Keeping that correctness while also staying fast under the crush is the problem this module solves.
Notes ↗motivation · Case Study: Taylor Swift Concert Sales on Ticketmaster
What actually overwhelmed Ticketmaster: load, not double-booking
The real failure was load, not correctness. About 12 million visitors and scalper bots drove 3.5 billion requests, four times the prior peak (violet). Under it, availability bent: queues froze thousands deep, Ticketmaster throttled and paused sales to stay up, and seat holds and payments timed out (red). But the transactions held: no seat sold twice, bots got nothing, and a record 2.4 million tickets sold in a day (green). Keeping that correctness while also staying fast under the crush is the problem this module solves.
Notes ↗motivation · Case Study: Taylor Swift Concert Sales on Ticketmaster
Size was never the problem: Visa and Amazon clear far more, reliably
Other systems clear far more, every day, without melting. Visa settles 100,000 transactions a second at peak; Amazon answers 105 million requests a second on Prime Day in under ten milliseconds. Ticketmaster fell over at four times its own prior record. The lesson is not that transactions are slow. It is that absorbing a sudden, massive spike of concurrent load takes the kind of engineering Visa and Amazon built over decades, and a one-off flash on-sale had not.
Each generation of the stack is about 100 times the last
Each generation runs roughly 10,000 times the load of the last (about 100× per decade), and concurrency, hardware, and data structures all jump together. The classic era ran thousands of transactions a second on disks and B-Trees; the modern era runs tens of millions on solid-state and LSM Trees; the agent era stacks a third jump on top, and has to hold the same correctness guarantees the classic era did.
ACID names four guarantees that turn a sequence of database writes into a transaction: Atomicity (all-or-nothing), Consistency (no broken invariants), Isolation (concurrent transactions don't see each other's partial work), and Du
3 figures
acid-properties · Example: Transactions in a Ticketmaster-like App
Alice's ticket purchase: three steps, all or nothing
Alice's purchase is one transaction around three steps: check inventory, verify the card, complete the sale. Atomicity binds them. All three commit as a unit, or a single failure rolls every one of them back, so a sale is never confirmed against a declined card or sold-out seats.
ACID: the four guarantees, the rule and the intuition for each
The four guarantees, one color per letter (the same circle reused on every ACID figure in the module). Each is a rule plus what it looks like when Alice buys a ticket: a declined card rolls back cleanly (atomicity), seats and revenue move as one (consistency), Alice locks the last seat so Bob waits (isolation), and a confirmed ticket survives a crash (durability).
Notes ↗acid-properties · Example 2: Banking Transfer Race Condition
Isolation prevents the lost update: $324 without it versus $318 with it
Two transactions race at month-end from the same start state: Alice $200, Bob $100, total $300. On the left (red), the transfer credits Bob first, so Bob already shows $200 while Alice is not yet debited and still shows $200; the interest job reads that half-done state and pays 6% on $400, the moving $100 counted twice, and the books end at $324, with $6 created from nothing. On the right (green), the interest job waits on the transfer's lock, reads the settled $300, and ends at the correct $318, exactly $18. The deeper point: isolation makes concurrent transactions appear sequential, as if the interest job simply ran after the transfer, so total money is conserved.
Every Stripe charge is a transaction that touches several tables (payment, ledger, merchant balance, webhook queue) and must satisfy all four ACID properties, or real money goes missing.
2 figures
case-study-transactions · What a Single Charge Touches
One Stripe charge is four writes, wrapped atomically
One Pay click is four writes: charge the card, write the double-entry ledger (two rows that must sum to zero), bump the merchant's balance, and queue a webhook. Stripe wraps them as one transaction, so all four land together or none do. The double-entry detail is why the ledger can never silently drift.
Notes ↗case-study-transactions · What Each Letter Buys You
Four ways a Stripe payment breaks, and the guarantee that stops each
Four ways a payment breaks, and the guarantee that stops each: a partial charge where the card is charged but the webhook never queues (atomicity), a ledger whose two rows stop summing to zero (consistency), two concurrent charges that race on one balance so one update is lost (isolation), and a confirmed charge lost when the server crashes (durability). The full definitions are on the ACID Properties page; here each letter is a real way Stripe loses money.
Every multi-step database operation that needs ACID guarantees is wrapped in BEGIN and COMMIT.
1 figure
transaction-examples · The Three Keywords
The three transaction keywords: BEGIN, COMMIT, ROLLBACK
The three keywords frame one block. BEGIN opens the transaction; your statements run inside as one logical operation; COMMIT makes every change since BEGIN permanent, or ROLLBACK undoes them all as if nothing happened. Rolling a transaction back is also called aborting it, the term the rest of this module uses (in Postgres, ABORT is the same command). Outside a block, each statement autocommits as its own one-statement transaction.
A database runs many transactions at once, and the hard part is coordinating how they touch shared rows.
4 figures
naive-concurrency · Naive way 1: run them one at a time
I/O lag: Read-start (Rs) at time 6 and Read-end (Re) at time 11 on resource B, with a ~5 unit gap of CPU idle time between them
One disk read (the dashed span between the two violet markers, its start and end) stalls the transaction far longer than the work around it. Under serial execution, nothing runs in that gap.
Notes ↗naive-concurrency · Naive way 1: run them one at a time
Run them one at a time: every wait is wasted
Serial execution wastes every wait. The violet blocks are transactions computing; the grey blocks are the server idle on disk. Because no one else may run in a gap, a long transaction (T2) stalls the whole queue: T3 and T4 wait far longer than their own work needs. The Ticketmaster on-sale would serialize into one slow line, even across unrelated rows.
Notes ↗naive-concurrency · Naive way 2: let them all run
Run them free: three ways the same data goes wrong
Uncontrolled concurrency corrupts one row three different ways. In a lost update, two transactions both read 100 and both write 101, so one play disappears. In a dirty read, one transaction reads another's uncommitted write and the writer then aborts, so the reader acted on a value that never existed. In a stale read, a transaction reads the same row twice and a committed write in between makes its two reads disagree.
Notes ↗naive-concurrency · The middle: concurrency control
The scheduling spectrum: safety on the left, performance on the right, the sweet spot in the middle
Concurrency control sits between the two naive ends, overlapping only the actions whose order does not matter. Serial (Naive 1), on the left, is correct but has no throughput; free-for-all (Naive 2), on the right, has throughput but corrupts data. The rest of this section makes "order does not matter" precise, then builds the engines that deliver it.
The correctness test ahead builds a directed graph from a schedule and checks it for a cycle.
2 figures
graph-refresher · Cyclic vs Acyclic Graphs
Side-by-side: an acyclic 3-node graph T1→T2→T3 vs a cyclic 3-node graph T1→T2→T3→T1
On the left, an acyclic graph: follow the arrows and you never return to a node, so the nodes line up in order. On the right, a cyclic graph: the loop means no consistent order exists. On the next page, a cycle in a schedule's graph marks the schedule unsafe.
A 4-node DAG and its two valid topological orderings, listed as text
The DAG has two valid topological orders, A → B → C → D and A → C → B → D; B and C are independent, so either can come first. Each is a safe sequence. On the next page, each valid topological order of a schedule's graph is an equivalent serial run.
The database reorders the actions of concurrent transactions to run them fast, but the outcome must still be correct: equivalent to running the transactions in *some* serial order.
12 figures
correctness · Key Definitions
Actions: a transaction's reads and writes
A transaction is a sequence of actions, each one a read or write of a single row, written T1.R(A) for transaction T1 reading row A. Blue boxes are reads, orange boxes are writes. Correctness tracks only their order, not the values they compute.
Serial runs one transaction's actions to completion, then the next. Interleaved mixes them in time. Serializable is an interleaving whose result matches some serial order. Blue boxes are reads, orange writes; the T1 or T2 prefix names the transaction, and the page tracks only the R/W order, the macro view.
Notes ↗correctness · Conflicts: The Heart of Concurrency Control
2 by 2 matrix of action pairs: only R-R is a non-conflict; the other three (R-W, W-R, W-W) are all conflicts
A conflict needs two transactions, the same row, and at least one write. Only read-read commutes; the other three each leave an order-dependent result, each a named anomaly: read-write a stale read, write-read a dirty read, write-write a lost update. Every conflict becomes a directed edge in the conflict graph, next.
Notes ↗correctness · How to Test for Conflict-Serializability
The conflict-serializability test: build a conflict graph, then check for a cycle
The whole test in three steps. Build one node per transaction, add a directed edge for every conflict, then look for a cycle. No cycle (green) means a serial order respects every edge, so the schedule is conflict-serializable; a cycle (red) means two edges demand opposite orders, so none does.
Notes ↗correctness · Example 1: Concert Ticket Booking
A serial schedule: T1, then T2, then T3, with no overlap
A serial schedule runs T1, then T2, then T3 with no overlap. The brackets on the left make the shape plain: each transaction's actions form one contiguous block, the visual signature of serial. It ends at A=9, B=20 with ACID intact, but with no parallelism it is the slowest of the three.
Notes ↗correctness · Example 1: Concert Ticket Booking
Even a serial schedule has conflicts, and they chain T1 to T2 to T3
Even a serial schedule has conflicts wherever transactions touch a shared variable: a W-R on A (T1 → T2), a W-W on B (T2 → T3), four in all. They chain T1 → T2 → T3, which is exactly the serial order that produced them.
Notes ↗correctness · Example 2: Interleaved Schedule Analysis
S1': the same three transactions, interleaved
S1' interleaves T1, T2, and T3 instead of running them serially. Whether the interleaving stays correct depends entirely on its conflict graph, built next.
Notes ↗correctness · Example 2: Interleaved Schedule Analysis
S1' has no cycle in its conflict graph, so the interleaving is safe
The conflict graph of S1' has four conflicts and no cycle, so S1' is conflict-serializable: it matches the serial order T2 → T1 → T3 (or T2 → T3 → T1). The faster interleaving is still correct.
Notes ↗correctness · Example 2: Interleaved Schedule Analysis
S2': a different interleaving, with an extra T2 write on B
S2' is a different interleaving of the same three transactions. The extra T2 write on B breaks it; the conflict graph next shows how.
Notes ↗correctness · Example 2: Interleaved Schedule Analysis
S2' has a cycle in its conflict graph, so the interleaving is unsafe
The conflict graph of S2' contains a cycle, so S2' is not conflict-serializable, and the interleaving is unsafe.
Sliding an interleaved schedule into a serial one by swapping non-conflicting neighbours
Can we swap an interleaved schedule into a serial one? The goal is to gather each transaction's actions into one contiguous block. T1's two tiles (violet) start apart, with a T2 action between them. A neighbour pair may swap only when it does not conflict; here the middle pair touches different rows, so one swap drops T1 into a block and T2 into the next, leaving the serial order T1 then T2. The write-write conflict on B (red) is the one pair that cannot move. A schedule untangles like this exactly when its conflict graph is acyclic.
Notes ↗correctness · The Landscape of Schedule Classes
Nested hierarchy: every S2PL schedule is a 2PL schedule, every 2PL schedule is conflict-serializable, every conflict-serializable schedule is serializable
Each ring is a set of schedules, strictly contained in the next: every conflict-serializable schedule is serializable, every two-phase-locking (2PL) schedule is conflict-serializable, and Strict-2PL sits inside 2PL. The gap between serializable and CS is a few rare blind-write schedules (a write that never reads its row), serializable yet not CS. The containment is also a difficulty gradient: the outer ring is checkable only by brute force, CS by the mechanical cycle test, and the 2PL rings, which the Two-Phase Locking page builds a few pages on, need no test at all, since the protocol enforces membership.
Concurrent transactions are independent multi-step operations running at the same time on shared data, and the database makes each one act as if no other transaction exists.
3 figures
concurrency-stack
The concurrency full stack: one COMMIT, four layers down to one instruction
One COMMIT, four layers down to a single instruction. The lock manager decides what to lock, the OS mutex decides how, and the hardware's compare-and-swap is the one indivisible step everything reduces to. The guarantee returns up the right side: swapped atomically, mutex held, row locked, transaction isolated.
Notes ↗concurrency-stack · Why OS vs Database Locks?
OS lock vs database lock manager: same word, different layer
The same word, two layers on one machine. The OS lock (grey) is one bit of state guarding a memory address it knows nothing about. The database lock manager (violet) locks at table, page, or row granularity, lets a transaction announce intent, and resolves deadlock for millions of transactions at once. The OS primitive cannot do any of that, so the database builds its own lock layer on top.
The lock manager: one component, two goals, three jobs
Every transaction asks one component, the lock manager, for its locks, and it keeps the lock table of who holds each row and who waits. It owes two goals: correct, equal to some serial order, and fast, overlapping the idle waits. Its first job is the one shown, granting a free row or queuing you behind the holder; scheduling many transactions and breaking deadlocks (greyed) each get their own page later.
A lock is a claim a transaction puts on a row before it reads or writes.
3 figures
lock-manager-cycle · One lock, start to finish
The lock lifecycle, in the schedule notation
One lock through its cycle, in the notation the schedules ahead use: a green flag is the request (Req), a red flag the grant (Get), a blue box the read, an orange box the write, and a purple flag the release (Unl). T1 takes row A, reads then writes while holding it, and releases. T2 wants the same row: it requests, waits the whole hold, and gets it only once T1 releases.
Notes ↗lock-manager-cycle · Two kinds of lock: shared and exclusive
Shared and exclusive locks
A shared lock (S) is for reading: several transactions can hold S on one row and read it together, because reads do not conflict. An exclusive lock (X) is for writing: only one transaction holds X, and everyone else waits.
Notes ↗lock-manager-cycle · Two kinds of lock: shared and exclusive
Lock compatibility matrix: shared and exclusive locks
The one compatible pairing is shared-with-shared; every cell touching an exclusive lock forces a wait. This is the conflict rule from the Correctness page made operational: two operations collide only when they touch the same row and at least one writes.
A transaction holds many locks across its life, and Two-Phase Locking (2PL) is the discipline for using them: every transaction grows its lock set first and shrinks it second, never reacquiring.
3 figures
db-locks-2pl · Two-Phase Locking (2PL): Growing then Shrinking
2PL: locks held rise during the growing phase, peak, then fall during the shrinking phase
2PL: the lock count only rises, then only falls. The peak is the moment the transaction holds everything it will ever need.
Notes ↗db-locks-2pl · Two-Phase Locking (2PL): Growing then Shrinking
Strict 2PL (S2PL): locks held rise during the growing phase, plateau, then drop to zero in one step at COMMIT
Strict 2PL: locks rise, plateau, then drop to zero in one step at commit. Nothing is released early.
Notes ↗db-locks-2pl · The Cost of Releasing Early: Cascading Aborts
Why strict 2PL stops cascading aborts
Release a lock early (regular 2PL) and another transaction can read work you later undo, so its abort cascades into theirs. Strict 2PL holds every lock to commit, so no one ever builds on uncommitted work. It is the sous-chef who hands over a sauce only once the head chef has approved it.
You met the lock manager and one lock's cycle; now it runs *many* transactions at once.
6 figures
end of lecture 1~69 min of figures
micro
The lock manager: the component that decides who waits and who runs
The lock manager again, all three jobs in one view. Job one, grant or queue, came on the Locks Stack page; this page is jobs two and three: the microschedule it builds as it grants locks, and the WaitsFor cycle it watches for. The rest of the page is those two jobs in motion.
Notes ↗micro · Reading the Timeline: A Read or Write Takes Time
A read or write takes time, and interleaving fills the gap
Every read and write on the lock manager's timelines below is drawn as a start and an end (Rs to Re, or Ws to We) because the data has to travel to storage and back. While T1's read of B is in flight, the CPU is idle, so the lock manager grants T2 in that gap. The whole point of a microschedule is to make those overlaps visible.
Notes ↗micro · Example 1: Serial Schedule (S1)
Serial lock schedule T2 → T1 → T3 (completion 49 units)
Serial schedule S1 runs T2, then T1, then T3 with no overlap. It is correct by construction but slow: 49 time units end to end, the baseline the interleaved schedules below have to beat. (The serial order is whichever the lock manager picks; any order is equally correct.)
Notes ↗micro · Example 2: Interleaved Schedule (S2) - Fast Execution
Interleaved lock schedule (completion 31 units)
When the lock manager interleaves the same transactions, S2 finishes in 31 units, well under the serial 49. Overlapping the lock-free stretches produces the speedup.
Notes ↗micro · Example 3: Interleaved Schedule (S3) - Different Microschedule
Interleaved lock schedule (completion 45 units)
S3 is a different valid interleaving of the same transactions, yet it finishes in 45 units, barely better than serial. Interleaving helps only when it overlaps the right operations; the order the lock manager grants in matters, not just the decision to interleave.
The same transaction pattern under different timing deadlocks: each transaction holds a ticket the other needs, so the lock manager's WaitsFor graph forms a cycle and neither can proceed. Interleaving buys performance only until a cycle stops everything.
Locking makes a reader wait for a writer to finish.
4 figures
mvcc · A familiar example: git
When you pulled decides what you see.
One repository changes over time, and your checkout is frozen at the moment you pulled. Engineer C commits a change, which adds v2 (v1 plus the diff, violet). Engineer D pulls afterward and gets v2; Engineer B pulled earlier, so B still has v1 and C's change does not touch B. Same repository, but when you pulled decides what you see.
An uncommitted write is invisible until it commits.
Minnie updates V at 2:45 but does not commit XY34 until 3:03. In that in-flight window the committed value is still AB1, so Pluto (snapshot 2:50) and Mickey (snapshot 3:00) both read AB1, never the half-written XY34. Only a query whose snapshot falls after the commit, like 3:05, reads XY34. The write becomes visible at the commit, not when it starts.
Notes ↗mvcc · Vacuum: reclaiming dead versions
Vacuum deletes the row versions that no running query still reads
A version is one value plus a timestamp, not a copy of the row. MVCC must keep an old version while any running query still reads it: here the query that opened at 2:50 still reads v3, and newer queries read v4, but nothing reads v1 or v2. Vacuum deletes the versions no running query needs, so the chain does not grow forever.
Notes ↗mvcc · The payoff: why OLTP and OLAP get separated at scale
Why OLTP and OLAP get physically separated at scale
MVCC removes the reader-writer lock contention, but a long analytical scan still strains shared resources on the production database: it saturates CPU and IO, pollutes the buffer cache, and pins VACUUM so tables bloat. None is a lock problem, so the fix is physical: replicate the data to a separate columnar warehouse for analytics and keep the OLTP database user-facing only.
Optimistic concurrency control (OCC) runs transactions with no locks.
1 figure
occ
Optimistic concurrency: run free, validate at commit, redo on a clash
Both transactions run lock-free and check only at commit. T1 validates clean and commits (green); T2 finds a row it read was written by T1, so it fails validation (red), aborts, and reruns. Under two-phase locking T2 would have waited; here the cost of never waiting is redoing the work on a clash.
A complete transaction system needs a correctness standard, *conflict-serializability*, and engines that target it.
1 figure
connecting-together · The Power of Transaction Managers: Beyond Simple Locks
The same two-phase locking, at every scale
The same protocol stretches three ways. It distributes, holding two-phase locking across machines in different cities; it recovers, replaying the write-ahead log after a crash so committed work survives; and it scales, running millions of transactions a second on stock exchanges and bank ledgers. Same core, different operating point.
A database keeps a log of what changed, never a copy of the whole database.
3 figures
recovery-intuition · The Version Problem
The version problem: everyone needs a past version, but full copies explode under concurrency, so a database logs the change instead
Keeping every version by copying the whole database (red) is billions of rows a second under real load, impossible. Logging each change instead (violet) is a handful of small entries, and any past version replays from them, 10:31:04 included. You reach any moment without ever storing a full copy.
Notes ↗recovery-intuition · You Already Use It
Google Docs version history is a log of edits, not fifty stored copies.
Google Docs keeps periodic snapshots and logs every edit between them, violet, not a full copy of every version. Any past version replays from a nearby snapshot forward. The same move a database log makes: save the change, reach any version.
Notes ↗recovery-intuition · You Already Use It
A git commit is a snapshot that shares unchanged files, not a full copy. Move forward to redo, backward to undo.
A Git commit is a snapshot, not a full copy: files that did not change are shared, so history stays small. Moving forward to a later commit redoes work, green; reverting one undoes it, red. A database recovery log gives the same two moves.
The lifecycle of one update: append to the log, update the RAM page, flush the page to disk later
One update, three writes in time order. ① Append the diff to the log (violet), durable at once; ② update A's page in the RAM buffer (red) in place, fast but volatile; ③ later, flush the whole 64 MB page to disk (green) as one write, start to end. Steps ① and ② happen now and the flush is deferred, so the log is always durable before the page reaches disk.
The log after two updates: A from 19 to 22.8 and B from 23 to 27.6. Each is one row with the old value (orange) and the new value (green), so the same log carries what it takes to put a change back or take it away. A commit record closes the transaction. These are the rows undo runs against when a transaction aborts, and redo when a crash hits.
Notes ↗recovery-concepts · Any Version, on Demand
The WAL log is the version history: replay it to any past state
The log read top to bottom is a full change history. To see the state at any past moment, stop replaying the rows there: just after T1, the version is A=22.8, B=27.6. Because each row carries both values, you can move either way, redoing new values forward or undoing old values back.
Notes ↗recovery-concepts · Cheap for Two Reasons: Small and Sequential
WAL turns scattered random writes into one sequential append
Without the log (grey), each change is a random I/O to its own scattered 64 MB data page, about 13 ms. With the log (violet), every change is a ~40 byte row appended to one growing file, one fast sequential stream: a 64 KB WAL page costs about 13 µs (~1,000× less), and a row in battery-backed NVRAM is durable in about 100 ns (~100,000× less). Small and sequential beats big and random.
Notes ↗recovery-concepts · Why the Log Goes First
Why log-before-data makes a crash recoverable
Data first (red): if the crash hits before the log row lands, the data changed with no row to replay, an inconsistency recovery cannot fix. Log first (green), the WAL order: a crash before the data page moves leaves the change sitting in the durable log, ready to re-apply. Log-before-data is the only order that keeps the data file recoverable.
A running transaction can abort, by choice or by error, and the database has to take back every change it made so it is as if the transaction never ran.
2 figures
recovery-wal · How Undo Works
Undo is logged: it appends W-abort rows to the log
T1's rows from the WAL Log page. When T1 aborts, undo does not just fix the data page, it appends its reversal to the log: a W-abort row for B (27.6 back to 23), then A (22.8 back to 19), then an abort record. The page ends at A=19, B=23. Because each reversal is itself a logged write, the log records the whole rollback, W-abort rows and all.
Notes ↗recovery-wal · The Hard Case: Uncommitted Data on Disk
The log for a long transaction TLong that aborts
TLong aborts. Undo walks the log backward, newest write first, restoring A1000 from 32.4 to 27, then A0 from 22.8 to 19, reusing the old value each forward row recorded. Each undo becomes its own W-abort row (red), so the log records the rollback itself. A final abort record closes the transaction.
After a crash, recovery answers one question from the WAL: which work survives and which is thrown away
A crash leaves recovery one question: which work survives, which gets thrown away. The whole answer is in the **WAL**. Committed transactions are kept, green; transactions that never committed are reversed, orange. Recovery is the procedure that reads the log and enforces both.
Notes ↗recovery-postcrash · Recovery on One Log: Down, Then Up
Crash recovery on one WAL: a forward replay rebuilds the crash state, then a backward undo reverses the uncommitted transactions
Recovery as two passes over one log:
Notes ↗recovery-postcrash · After the Crash: Fix the Disk from the WAL
Just after the crash: RAM is gone and the disk is an inconsistent mix of flushed and unflushed work
Just after the crash. RAM, red, is gone with every in-flight change. The disk, green, survived but is not consistent: a committed write is missing, and an uncommitted write is wrongly present. The WAL, violet, is untouched, and it is the only thing recovery can use, because RAM holds nothing.
Recovery restores two ACID guarantees across a crash: durability from the replay, atomicity from the undo pass, both resting on the write-ahead rule
Two ACID guarantees fall out of the two passes, in the order the passes run. Durability, the violet D, comes from the replay: once you see COMMIT, that work is replayed and never undone, so it survives any failure. Atomicity, the blue A, comes from the undo pass: a transaction is all or nothing, so unfinished work is reversed. Both rest on the write-ahead rule at the base, the log reaching disk before the data.
A Transaction Manager is the orchestrator that runs scheduling, locking, write-ahead logging, deadlock detection, and recovery as one cooperating system, making thousands of concurrent transactions look like each ran alone, with t
3 figures
tm-summary · The Architecture
Transaction Manager architecture: incoming transactions flow through five cooperating components into one consistent ACID database state
Five components, one guarantee. The scheduler keeps the interleaving conflict-serializable (Consistency); the lock manager enforces 2PL with O(1) hash lookups (Isolation); the WAL logs every change before the data page moves (Durability); the deadlock detector watches the waits-for graph and aborts a victim on a cycle; the recovery manager runs Analysis, REDO, and UNDO after a crash (Atomicity). Together they turn concurrent transactions into one consistent state.
Notes ↗tm-summary · Five Components, Four Firing
A no-conflict trace: three transactions on different tuples flow through the Transaction Manager, four of the five components fire
One coherent execution. The scheduler routes each transaction to a thread, the lock manager grants all three locks with no waiting (the rows do not overlap), and the WAL logs every change before its commit becomes durable. The deadlock detector stays quiet because there is no waits-for cycle, and the recovery manager only acts if the server crashes. ACID preserved.
Why the Transaction Manager scales: most transactions touch different data, so few of them ever conflict
Most transactions never touch the same row, so most never wait. Out of roughly a thousand concurrent transactions, only ten to twenty actually conflict. A hash table makes each lock check O(1), the WAL turns scattered updates into one sequential stream, and each component does one job behind a tiny interface. That separation of concerns lets a database run a million transactions per second on commodity hardware.
Run three concurrent transactions through the whole Transaction Manager at once and watch the components cooperate: the scheduler interleaves them for speed, the lock manager keeps them off each other's rows, and the WAL makes eve
3 figures
tm-three-transactions · The Setup: T1, T2, T3 Operating on A, B, C
The interleaved schedule of T1, T2, T3 on A, B, C
One interleaved schedule, eight reads and writes in step order. T1 and T2 share A, T3 owns B and C, so the scheduler is free to interleave them. Reads are blue, writes orange, and the Value column is the variable right after each step. Under Strict 2PL every lock is held until COMMIT or ABORT, which is what keeps this interleaving safe.
Notes ↗tm-three-transactions · Scenario 1: All Transactions Commit
Scenario 1: all three transactions commit
Scenario 1: all three commit. Each write is logged to the WAL, then a COMMIT record follows, so the change is durable the moment the commit lands. Strict 2PL holds each lock until that COMMIT, so no transaction ever read another's uncommitted data. The database advances to one new consistent state: A=27.36, B=38.4, C=32.4, all durable (the violet D for Durability).
Notes ↗tm-three-transactions · Scenario 2: Mixed Outcomes, T3 Aborts
Scenario 2: T3 aborts while T1 and T2 commit, with no cascade
Scenario 2: T3 aborts while T1 and T2 commit. The recovery manager reads T3's WAL entries backward and restores the old values, reverting C from 32.4 to 27 and B from 38.4 to 32, so T3 leaves no trace (the blue A for Atomicity). The final state is A=27.36, B=32, C=27.
A correct transaction manager still falls over under real load.
4 figures
problem-solving · The Core Problems
Ticketdisaster.com checkout flow under Eras Tour load
The Ticketdisaster.com checkout flow under Eras Tour load. The lock acquires the instant a user picks a zone and stays held through browsing and payment, so one slow user freezes a thousand seats for minutes. The three worst bottlenecks come next.
problem-solving · The Smart Solutions: Ticketsfaster.com
Fix two: shrink the lock
Shrink the lock. A zone lock freezes a thousand seats, so nine hundred ninety-nine other fans wait on one. A seat-level lock blocks nobody else, and about a hundred times more fans browse at once.
Notes ↗problem-solving · The Smart Solutions: Ticketsfaster.com
Fix one: cut lock duration
Cut lock duration. The old flow holds the zone lock through browsing and payment, five to ten minutes, so only about a hundred fans run at once. The fix does the slow steps in the queue with no lock and takes the lock only for the final write, about thirty seconds, so about ten thousand fans run at once.
Notes ↗problem-solving · The Smart Solutions: Ticketsfaster.com
Fix three: fall back to async
Fall back to async. Instead of blocking in real time, fans submit instantly with a timestamp, a background job processes them in order, and the confirmation arrives by email. The three-hour queue becomes a sixty-second submit.
Strict 2PL gives safe, correct concurrency and runs most production databases today, but it refuses many schedules that are perfectly serializable.
4 figures
next-100x
Where the next 100x comes from: not a faster Strict 2PL, but systems that drop it
Scaling runs about 100x per decade. Strict 2PL, blue, is where production databases run today. The next 100x, violet, comes from protocols that drop Strict 2PL and admit the schedules it refuses; tuning 2PL itself, grey, does not scale.
Nested hierarchy: every S2PL schedule is a 2PL schedule, every 2PL schedule is conflict-serializable, every conflict-serializable schedule is serializable
Every ring outside S2PL is safe, valid schedules that today's S2PL databases refuse to run. The Serializable − CS ring and the CS − 2PL ring are real schedules a smarter scheduler could admit; that untapped gap is the 100x opportunity.
Notes ↗next-100x · Match the Protocol to the Workload
Choosing a path by workload signature: each signature maps to one protocol, the real systems that use it, and a case study at scale
Each workload signature, grey, maps to one protocol, violet, and the real systems that ship it, blue. The signature is the case: a viral counter, a play stream, a shared graph.
The takeaway: Strict 2PL is the baseline, the next 100x lives in the schedules it refuses, the same correctness math holds, and next the machine splits into many
Strict 2PL, violet, is the baseline, and the next 100x lives in the safe schedules it refuses. What defines correct is serializability, green: a smarter scheduler can admit schedules that are serializable yet not even conflict-serializable. Every protocol here ran on one machine; distributed systems break that machine into a thousand, with the data, the locks, and the clock on different computers.