Transactions
really means abortability: on any fault mid-transaction, all its writes are undone, so retrying is safe.
is a property of the application's invariants (money is conserved). The database can only help via A and I.
concurrent transactions don't step on each other; ideally as if they ran one at a time (see the playground below).
once committed, data survives crashes: the commit record is fsynced (or replicated) before the client hears “ok”.
Transfer $100 from A to B
- ✓UPDATE accounts SET bal = bal - 100 WHERE id = 'A'⚡ crash
- ·UPDATE accounts SET bal = bal + 100 WHERE id = 'B'
- ·COMMIT → client is told “ok”
Try: crash after the debit with atomicity off, then on. Then crash just after “ok” with durability off: a lie to the client that no amount of atomicity can fix.
The isolation playground
dirty-readdirty-writeread-skewwrite-skewphantomread-committedpredicate-lockserializable-isolationWrite skew · snapshot isolation
Alice and Bob are both on call and both feel sick. Each checks that at least two doctors are on call, then goes off call.
- Press Step to run the schedule one operation at a time.
Database
Which isolation level prevents which anomaly? (each cell = this engine actually running the schedule)
| no isolation | read uncommitted | read committed | snapshot isolation | serializable (2PL) | serializable (SSI) | |
|---|---|---|---|---|---|---|
| Dirty read | ||||||
| Dirty write | ||||||
| Read skew | ||||||
| Lost update | ||||||
| Write skew | ||||||
| Phantom |
Try: write skew under snapshot isolation (both doctors go off call), then the same schedule under 2PL (deadlock, one aborts) and SSI (the second committer is aborted). Click any cell to load it.
Row versions on disk
txn 13 commits. New transactions will see $600/$400; 12 and 14 still won't.
| key | balance | created_by | deleted_by | txn 12 sees it? |
|---|---|---|---|---|
| acct 1 | $500 | 3 | 13 | ✓deletion ignored (rule 3: 13 started after 12) |
| acct 2 | $500 | 3 | 13 | ✓deletion ignored (rule 3: 13 started after 12) |
| acct 1 | $600 | 13 | — | ✗creation hidden (rule 3: 13 started after 12) |
| acct 2 | $400 | 13 | — | ✗creation hidden (rule 3: 13 started after 12) |
Try: step to 8 and switch between txn 12, txn 14 and a new transaction. Three readers, three answers, one set of rows and no read locks. Then step to the end to watch garbage collection.
Transactions
4 clients × 6 increments · read-modify-write
Try: read-modify-write with 6+ clients and short think time — count the red bars. Switch to SELECT FOR UPDATE: no losses, but the lanes fill with amber waiting. Compare-and-set trades waiting for retries.
Time to finish all increments
Serializability #1: actually run them one at a time
actual-serial-executionOne thread per partition, one transaction at a time
Try: switch to interactive: throughput falls ~50× because the single thread idles during every network round trip. Then go back, add cores, and drag cross-partition up to 30%: the red stalls eat most of the gain.
Transactions
Lock table
| object | holders | wait queue |
|---|---|---|
| A | — | |
| B | — | |
| C | — |
Wait-for graph
Log
- Click Step on a transaction to run its next operation.
Try: Step T1, T2, T1, T2: each holds what the other wants. The detector finds the cycle and aborts T2; restart it after T1 commits.
Committed transactions per 100 ticks vs contention
Try: drag writes to 0%: readers never conflict, so both lines reach the ceiling. Then raise writes and shrink the hot set: 2PL burns time waiting and breaking deadlocks, SSI burns it on aborted work. Serial execution ignores contention entirely, but only scales with the speed of a single core.
- takeawayAtomicity is abortability. It turns a crash halfway through into a clean error you can retry. Durability means the commit is on disk or replicated before the client hears “ok”.
- takeawayWeak isolation levels each let something through. Read committed allows read skew and lost updates. Snapshot isolation still allows write skew and phantoms. Learn which level your database actually defaults to.
- takeawayTrue serializability comes three ways. Serial execution is fast only for tiny in-memory transactions. 2PL blocks and deadlocks under contention. SSI never blocks but aborts more as conflicts rise.
Key concepts · 20
book chapter 7- ACID
Atomicity, Consistency, Isolation, Durability: the safety guarantees a transaction is supposed to provide.
The term is more marketing than definition: implementations differ widely, and 'isolation' in particular rarely means serializable by default.
↑ see it in the visualization - Atomicity (abortability)
If a transaction can't finish, every write it made is discarded; it's all or nothing.
It has nothing to do with concurrency. Its value is that after a fault the application can safely retry instead of guessing which writes landed.
↑ see it in the visualization - Consistency (in ACID)
Application-defined invariants (e.g. credits equal debits) hold before and after every transaction.
It's a property of the application, not the database: the database can't know your invariants, it can only give you atomicity and isolation to preserve them.
↑ see it in the visualization - Durability
Committed data is not lost, even after a crash: written to non-volatile storage (with a write-ahead log) or replicated before acknowledging.
Perfect durability doesn't exist (disks fail, fsync lies, correlated outages); it's a matter of reducing risk, usually by combining disks and replication.
↑ see it in the visualization - Dirty read
A transaction sees another transaction's writes before they've committed.
The other transaction may still abort, so you'd act on data that never officially existed. Prevented by read committed and above.
↑ see it in the visualization - Dirty write
A transaction overwrites a value that another uncommitted transaction has written.
Interleaved writes across rows (the car sold to Bob but invoiced to Alice). Almost every database prevents it with row-level write locks held until commit.
↑ see it in the visualization - Read committed
Isolation level that prevents dirty reads (show the last committed value) and dirty writes (row write locks).
The default in PostgreSQL, Oracle and SQL Server. It still allows read skew, lost updates and write skew.
PostgreSQLOracleSQL Server↑ see it in the visualization - Read skew (non-repeatable read)
A transaction sees different parts of the database at different points in time, e.g. one account before a transfer and the other after.
Tolerable for a single page view, fatal for backups and long analytic queries, which would observe a state that never existed.
↑ see it in the visualization - Snapshot isolation
Each transaction reads from a consistent snapshot of the database as of when it started.
Readers never block writers and writers never block readers. Called 'repeatable read' in PostgreSQL and MySQL and 'serializable' in Oracle, though it isn't serializable.
PostgreSQLMySQL InnoDBOracleSQL Server↑ see it in the visualization - Multi-version concurrency control (MVCC)
Keep several committed versions of each row, tagged with created_by and deleted_by transaction IDs, so different transactions can see different points in time.
Visibility rules decide which version each reader sees, which is how snapshot isolation is implemented without read locks; garbage collection removes versions nobody can see.
↑ see it in the visualization - Lost update
Two transactions read-modify-write the same value concurrently, and one overwrites the other's change.
The classic counter-increment bug. Fix it with atomic updates, explicit locks (SELECT FOR UPDATE), compare-and-set, or a database that detects it and aborts.
↑ see it in the visualization - Compare-and-set
Write a new value only if the current value still equals what you read; otherwise retry.
Avoids locks, but under contention most of the work is spent on retries. Unsafe if the 'compare' reads from an old snapshot.
↑ see it in the visualization - Write skew
Two transactions read the same objects, each decides based on what it read, then they update different objects, together breaking an invariant.
A generalization of lost update that snapshot isolation does not prevent (doctors on call, double-spending, unique usernames). Needs serializable isolation or explicit locks on the rows read.
↑ see it in the visualization - Phantom
A write in one transaction changes the result of a search query in another, typically by inserting a row that didn't exist when the search ran.
There's no row to lock when the check is 'no booking exists'; you need predicate or index-range locks, materialized conflicts, or SSI.
↑ see it in the visualization - Serializability
The strongest isolation level: the result is the same as if transactions had run one at a time, in some order.
Prevents every race condition above. Three ways to get it: actual serial execution, two-phase locking, and serializable snapshot isolation.
↑ see it in the visualization - Actual serial execution
Run every transaction one at a time on a single thread, with data in memory and transactions submitted as stored procedures.
No concurrency control at all, but throughput is capped at one CPU core per partition, and cross-partition transactions are much slower.
VoltDBH-StoreRedisDatomic↑ see it in the visualization - Two-phase locking (2PL)
Readers take shared locks and writers exclusive locks, all held until the transaction ends (grow, then shrink).
Pessimistic: readers block writers and writers block readers. Serializable, but with unstable latency, low throughput under contention, and frequent deadlocks.
MySQL serializableSQL Server serializableDB2↑ see it in the visualization - Predicate and index-range locks
Locks on all objects matching a search condition, including rows that don't exist yet; usually approximated by locking a range of an index.
What lets 2PL prevent phantoms such as double-booked meeting rooms.
↑ see it in the visualization - Deadlock
Transactions each waiting for a lock the other holds, forming a cycle in the wait-for graph.
The database detects the cycle and aborts one transaction, which the application must retry. Under 2PL this happens often.
↑ see it in the visualization - Serializable snapshot isolation (SSI)
Snapshot isolation plus tracking of which transactions read data that a concurrent transaction wrote; on a dangerous pattern, abort at commit.
Optimistic: no blocking, readers and writers proceed freely. Performs well under low contention, but abort rates rise as contention grows.
PostgreSQL serializableFoundationDBCockroachDB↑ see it in the visualization