Skip to main content

Isolation Levels & Read Anomalies

Level 2: Level 2: Data Storage & Modelinghard35 minisolationconcurrencytransactions

Diagnose the exact anomaly (dirty read, lost update, write skew) and fix it surgically; snapshot isolation still allows write skew, which only Serializable or a lock prevents.

A menu of how much transactions see of each other

Isolation levels are a menu of how much concurrent transactions are allowed to see of each other's in-flight work. The ANSI SQL levels, weakest to strongest, are Read Uncommitted, Read Committed, Repeatable Read, and Serializable. Each level forbids more anomalies and costs more throughput.

The anomalies, in order of nastiness

  • Dirty read: you read another transaction's uncommitted write, which may roll back. Forbidden at Read Committed and above. Almost no real database defaults to allowing it.
  • Non-repeatable read: you read a row, another transaction commits an update to it, you read it again in the same transaction and get a different value. Forbidden at Repeatable Read and above.
  • Phantom read: you run a range query (WHERE status = 'pending'), another transaction inserts a new matching row, you re-run and a new row appears. Classically forbidden only at Serializable.
  • Lost update: two transactions read the same value, both compute a new value from it, both write; one overwrites the other. balance = balance + x done as read-modify-write in the app is the usual victim.
  • Write skew: two transactions each read an overlapping set, each individually keeps an invariant true, but their combined effect violates it. The textbook case: two doctors both on call, each checks "at least one other doctor is on call" (true), each takes themselves off shift, and now zero doctors are on call.

Defaults matter and differ: Postgres defaults to Read Committed, MySQL InnoDB defaults to Repeatable Read. So the same application code can behave differently on the two databases under load, which is a real production trap.

Check yourself
Two doctors run the on-call transactions under snapshot isolation (what Postgres calls Repeatable Read). Each reads a consistent snapshot showing the other still on call, then removes itself. Can both commit, leaving zero doctors on call?

The most-probed subtlety: snapshot isolation and write skew

Snapshot isolation (what Postgres calls Repeatable Read, and what many systems mean by "Repeatable Read") gives every transaction a consistent snapshot as of its start, which kills dirty, non-repeatable, and phantom reads for reads. But snapshot isolation still permits write skew, because each transaction validates against a stale snapshot that does not see the other's write. Only true serializable forbids write skew, implemented either by Serializable Snapshot Isolation (SSI, which detects dangerous read-write dependency cycles and aborts one transaction) or by strict two-phase locking (2PL). Serializable costs throughput: SSI adds abort-and-retry churn under contention, 2PL adds lock waits.

Interview nuance: If you claim "Repeatable Read fixes it," the interviewer will ask "which anomaly, and are you sure Repeatable Read covers it?" If the bug is write skew, Repeatable Read (snapshot) does not fix it and you need Serializable or an explicit lock. Naming the exact anomaly is the whole game.

Fixing a concurrency bug without global Serializable

  • SELECT ... FOR UPDATE: take a pessimistic row lock on the rows you are about to modify, forcing concurrent transactions to queue. Fixes lost update and many oversell cases with surgical scope.
  • Optimistic version column: add version, read it, and on write do UPDATE ... WHERE version = :read_version; if 0 rows change, someone beat you, so retry. Cheap under low contention.
  • Unique constraint: sometimes the cleanest fix. If "one seat per booking" must hold, a unique index enforces it regardless of isolation level.
Check yourself

Pick the surgical fix for each concurrency bug, without turning on global Serializable.

Flash-sale oversell on the last unit in stock; concurrent buyers must queue on that row
Two admins occasionally edit the same record and one save silently overwrites the other
Exactly one booking per seat must hold no matter what any application code does

Recap: pick the isolation level (or explicit lock) by naming the exact anomaly; snapshot isolation stops dirty, non-repeatable, and phantom reads but still allows write skew, which only true Serializable or targeted locking prevents.

Check yourself
A teammate proposes ending all concurrency bugs by running the whole database at Serializable. What is the sharpest objection?

Apply

Your turn

The task this lesson builds to.

Choose an isolation level (or explicit locking) that prevents a checkout from overselling inventory under load, and justify the concurrency cost.

Think about

  1. Which anomaly (lost update, write skew, phantom) is causing the oversell?
  2. What is the difference between snapshot isolation and true serializable?
  3. How do SELECT ... FOR UPDATE or a version column fix it?

Practice

Make it stick

A second problem on the same idea, so it survives past today.

Design the concurrency control for a Ticketmaster-style seat-reservation system during an onsale where 50,000 users race for 20,000 numbered seats in the first 30 seconds, so no seat is ever sold twice and the invariant 'a seat has at most one active reservation' holds under write skew.

Think about

  1. What is the strongest, cheapest layer that can enforce per-seat uniqueness regardless of isolation level?
  2. Why would global Serializable collapse throughput exactly at peak load?
  3. What role can Redis play without ever becoming the source of truth?