0%
Chapters / Transactions
CHAPTER 07

Transactions

A transaction is a promise to make a set of changes look atomic to everyone else, at a cost you get to choose.

source: DDIA, Chapter 7

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

mvcc-store.ts
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);
  }
}
Tip
Level 2 (Correctness) would add a write-write conflict check at commit time: if another transaction wrote the same key and committed after this transaction's snapshot was taken, abort - otherwise a lost update slips through even under this scheme.

7.5Failure Modes

ConditionResulting state
Two transactions each read a shared invariant, then each independently perform a write that's individually validWrite 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 missingA 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 collectedEvery 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 levelPreventsStill allowsCost
Read committedDirty reads, dirty writesLost updates, write skew, non-repeatable readsLow - minimal locking, high concurrency
Snapshot isolationDirty reads, lost updates (with conflict check), non-repeatable readsWrite skewModerate - version storage and snapshot bookkeeping
SerializableAll of the above, including write skewNothing - behaves as if transactions ran one at a timeHigh - 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.