7.1Chapter Thesis
A transaction is a promise: a group of reads and writes will appear, to every other observer, as if it happened all at once or not at all. That promise is not free - the stronger you make it, the more concurrency you give up to enforce it. Isolation level is the dial that trades correctness guarantees for throughput, and most production bugs involving "impossible" data states trace back to that dial being set lower than the application actually assumed.
7.2First Principles
Concurrency anomalies exist because operations interleave
Without isolation, two transactions running "at the same time" are really interleaving arbitrarily at the instruction level. A dirty read sees another transaction's uncommitted write; a lost update happens when two transactions read-modify-write the same value and one overwrite silently discards the other's change; write skew happens when two transactions each read a consistent snapshot, each individually make a decision that's valid given what they read, and the combination of both decisions violates an invariant neither transaction could see on its own.
Snapshot isolation trades serializability for concurrency
Rather than locking rows and blocking concurrent access, multi-version concurrency control (MVCC) gives every transaction a consistent snapshot of the database as it existed at the transaction's start, and lets writes proceed by creating new versions rather than overwriting existing ones. This eliminates dirty reads and lost updates (with a write-conflict check) without requiring readers to block writers at all - but it does not eliminate write skew, because two transactions can each read a valid snapshot and write non-conflicting keys while still jointly violating a cross-row invariant.
7.3Mechanism
Every write is tagged with the id of the transaction that created it rather than overwriting the previous value in place. A read within transaction T only considers versions created by transactions that committed before T started - this is the entire mechanism that makes a snapshot "consistent": it's a filter over version history, not a special frozen copy of the data.
7.4Build It Yourself
interface Version { txId: number; value: string; } class MvccStore { private versions = new Map<string, Version[]>(); private nextTxId = 1; private committed = new Set<number>(); begin(): { txId: number; snapshotOf: number[] } { const txId = this.nextTxId++; return { txId, snapshotOf: [...this.committed] }; } // only versions from transactions already committed at // snapshot time are visible - this is the whole definition // of "snapshot". read(key: string, snapshotOf: number[]): string | undefined { const visible = (this.versions.get(key) ?? []) .filter((v) => snapshotOf.includes(v.txId)); return visible.at(-1)?.value; } write(key: string, value: string, txId: number): void { const list = this.versions.get(key) ?? []; list.push({ txId, value }); this.versions.set(key, list); } commit(txId: number): void { this.committed.add(txId); } }
7.5Failure Modes
| Condition | Resulting state |
|---|---|
| Two transactions each read a shared invariant, then each independently perform a write that's individually valid | Write skew: both commit successfully under snapshot isolation, and the combined result violates an invariant neither transaction could observe, because neither wrote a key the other read. |
| The write-conflict check is skipped or missing | A lost update occurs silently: two transactions read the same value, each compute a new value from it, and the second commit overwrites the first with no error. |
| Old versions are never garbage collected | Every key's version list grows unbounded, and every read must filter a longer and longer list, degrading read latency over the life of the database. |
7.6Tradeoffs
| Isolation level | Prevents | Still allows | Cost |
|---|---|---|---|
| Read committed | Dirty reads, dirty writes | Lost updates, write skew, non-repeatable reads | Low - minimal locking, high concurrency |
| Snapshot isolation | Dirty reads, lost updates (with conflict check), non-repeatable reads | Write skew | Moderate - version storage and snapshot bookkeeping |
| Serializable | All of the above, including write skew | Nothing - behaves as if transactions ran one at a time | High - requires either true serial execution, strict locking, or conflict detection covering read sets |
7.7Production Perspective
Real databases like PostgreSQL implement snapshot isolation as the default under the name "Repeatable Read," and offer true serializable isolation via serializable snapshot isolation (SSI), which tracks read/write dependencies between concurrent transactions to detect the exact write-skew pattern this chapter's minimal store cannot catch, aborting one transaction instead of allowing both to commit.