skip to content

How do database engines actually implement SERIALIZABLE isolation, and what does each approach cost the application that uses it?

level: seniorimportance: should knowfreq 48%

answer

  1. Strict 2PL: locks held to commit + range/predicate locks
  2. Blocking and deadlocks vs aborts and retries
  3. SSI: snapshot + dangerous read-write dependency structure
  4. Conservative detection = false-positive aborts
  5. Short transactions, narrow read sets, central retry loop

basics

~20 s

Two families: pessimistic two-phase locking with range/predicate locks, which serializes by blocking and produces deadlocks; and optimistic serializable snapshot isolation, which never blocks readers but aborts transactions whose read/write dependencies could not come from any serial order. Optimistic requires an application retry loop.

solid answer

~50 s

**Pessimistic (strict two-phase locking).** Reads take shared locks, writes exclusive ones, and everything is held to commit. To stop phantoms, predicate or next-key/range locks cover the gaps a query examined, not just the rows returned. Correct and blocking-based: hot rows queue, lock waits become latency, and cycles surface as deadlocks with a victim rollback. **Optimistic (serializable snapshot isolation).** Each transaction reads a consistent snapshot and never blocks readers. The engine tracks read/write dependencies and looks for the dangerous pattern that makes a schedule non-serializable — a transaction with both an inbound and an outbound read-write conflict — then aborts one participant with a serialization failure. The application costs differ. Locking costs throughput and requires consistent access ordering to limit deadlocks. SSI costs aborts: **every** transaction path needs an idempotent retry loop, false positives exist, and abort rates climb with contention and with long-running readers. Both make keeping transactions short the primary tuning lever.

code

text · 9 lines
text
attempt = 0
loop:
  BEGIN ISOLATION LEVEL SERIALIZABLE
    ... reads and writes ...
  COMMIT
  on serialization_failure:
     attempt += 1
     if attempt > MAX: give up, surface error
     sleep(backoff with jitter); restart loop  // re-run the logic, not just COMMIT

go deeper

for a junior

Know that SERIALIZABLE is enforced either by holding locks until commit or by aborting conflicting transactions, and that retries may be necessary.

for a middle

Explain predicate/range locking as what stops phantoms in the lock-based approach, and describe the snapshot-plus-conflict-detection alternative.

for a senior

Diagnose from the symptoms: lock waits and deadlocks versus serialization-failure rates, and prescribe short transactions, narrower read sets, indexes and a central retry loop.

for a principal

Weigh a global SERIALIZABLE policy against targeted constraints and atomic statements, define the retry contract and abort-rate SLOs, and account for how the choice constrains scaling and replica routing.

## Why implementation matters SERIALIZABLE is defined by an outcome — the schedule must be equivalent to some serial order — not by a mechanism. Two very different mechanisms deliver it, and they fail in opposite ways. Knowing which one you are on determines whether you tune for blocking or write retry code. ## Pessimistic: strict two-phase locking Two-phase locking (2PL) has a growing phase, where a transaction acquires locks, and a shrinking phase, where it releases them. *Strict* 2PL holds every lock until commit or rollback, which additionally guarantees recoverability — nobody reads data that might be rolled back. Row locks alone give you repeatable reads but not serializability, because a concurrent insert can add a row matching a predicate you already evaluated. To close that, the engine must lock the *predicate*, not just the rows. Real systems approximate predicate locks with index-range locks (often called next-key or gap locks): scanning `WHERE status = 'PENDING'` locks the index range covering `PENDING`, so an insert into that range must wait. Costs to the application: - **Blocking is the throughput ceiling.** A contended row or a hot index range effectively serializes those transactions. Latency percentiles, not averages, are where this shows up. - **Deadlocks are normal, not exceptional.** Two transactions taking the same locks in different orders form a cycle; the engine kills a victim. Mitigations: touch resources in a consistent order, keep transactions short, avoid user think-time inside a transaction, and still handle the deadlock error. - **Lock escalation and range locks surprise people.** Locking a range can block inserts of rows that do not exist yet — correct, but invisible in the table's current contents. ## Optimistic: serializable snapshot isolation SSI builds on multi-version concurrency control. Every transaction reads a consistent snapshot of committed data as of its start, so readers never block writers and writers never block readers. That alone gives snapshot isolation, which is *not* serializable. SSI adds detection. The theory result it relies on: every non-serializable execution under snapshot isolation contains a transaction with both an incoming and an outgoing **read-write dependency** (someone wrote data it read, and it wrote data someone else read) with those two conflicts adjacent in the dependency graph. The engine tracks read sets — using index range information so predicates are covered too — and when that dangerous structure appears, it aborts one of the participants with a serialization failure error. Costs to the application: - **Mandatory retry loop.** Any transaction can fail at commit with a retryable error. The retry must re-execute the transaction's logic, not just re-issue the commit, and side effects (emails, external calls) must not have escaped before commit. - **False positives.** Detection is conservative: some aborted transactions were in fact serializable. That is the price of tracking approximate read sets rather than exact predicates. - **Contention sensitivity.** Under high write contention on overlapping read sets, abort-and-retry becomes livelock-shaped throughput collapse. Backoff, shorter transactions and narrower read sets are the tuning levers. - **Long-running readers.** Read-only transactions extend the window in which dependencies must be tracked, increasing both memory for tracking state and abort probability elsewhere. ## Choosing and operating Guidance that holds either way: 1. **Keep transactions short and free of network waits.** This is the single biggest lever for both mechanisms. 2. **Shrink read sets.** A transaction that scans a whole table under SERIALIZABLE conflicts with almost every writer, whichever implementation you use. Add the index that lets it touch fewer rows or a narrower range. 3. **Write the retry loop once, centrally**, with jitter and an attempt cap, and make transactions idempotent so a retry is safe. 4. **Read-only transactions should be declared read-only.** Both families can then relax tracking; several engines fast-path them entirely. 5. **Consider the cheaper alternative.** Many invariants that people reach for SERIALIZABLE to protect are better expressed as a unique constraint, a check constraint, an atomic conditional update, or an explicit row lock on the one row that guards the invariant. Those cost far less than raising the level for the whole workload. Finally, measure. Lock-wait time and deadlock counts characterize the pessimistic world; serialization-failure rate and retry counts characterize the optimistic one. Whichever you run, that metric belongs on a dashboard before you rely on SERIALIZABLE in production.

  • Under an optimistic serializable implementation, why is it wrong to retry by simply re-issuing COMMIT?
    A serialization failure aborts the whole transaction, so all of its reads and writes are gone. The retry must re-execute the business logic against a fresh snapshot, because the decision the logic made was based on data the engine has now declared inconsistent with any serial order. Re-issuing only the commit would either error or commit nothing.
  • You see rising deadlock counts after moving a workload to SERIALIZABLE on a lock-based engine. What do you check first?
    Look at which lock modes and ranges the conflicting statements take, and whether transactions acquire resources in different orders. The usual fixes are ordering access consistently (for example always by primary key ascending), shortening transactions so lock hold time drops, and adding indexes so range locks cover narrow key ranges instead of large scans. Deadlock retries still need handling regardless.
  • Does SERIALIZABLE guarantee that transactions appear to run in the order clients submitted them?
    No. Serializability only requires equivalence to some serial order, which need not match real-time submission order. The stronger property is strict serializability, which adds the real-time constraint. Most single-node engines behave strictly in practice, but if your architecture routes reads to replicas you can observe orderings that plain serializability permits.

Pessimistic locking is a booking system that reserves the seat while you decide; optimistic serialization lets everyone browse freely and cancels a booking at checkout when two choices turn out to be incompatible.

saying these in an interview costs you the question

  • Assuming SERIALIZABLE always means locking, or always means snapshots
  • Using SERIALIZABLE without any retry handling on an optimistic engine
  • Believing an aborted transaction proves an actual conflict occurred (detection is conservative)
  • Thinking read-only transactions are free under SERIALIZABLE
  • Treating deadlocks as a bug to eliminate entirely rather than an expected, retryable outcome

context