30-Second Glance
- 2PL = shared (read) locks + exclusive (write) locks, with no lock released until commit/abort: the classic pessimistic route to serializability.
- Readers block writers and writers block readers on the same row — correctness by waiting, throughput paid in lock latency.
- Predicate/phantom protection needs range locks (InnoDB next-key locks); row locks alone still allow phantoms.
- Deadlocks are inherent: engines detect cycles (wait-for graph) and abort a victim — app must retry.
The Protocol
Two phases, strictly ordered per transaction:
- Growing phase — acquire locks as needed:
SHARED(S) for reads you want repeatable,EXCLUSIVE(X) for writes. S↔S compatible; anything involving X conflicts. InSERIALIZABLEmode (InnoDB, SQL Server) plain SELECTs take S locks; at weaker levels onlySELECT ... FOR UPDATE/SHAREand DML lock. - Shrinking phase — release everything, but only at commit/abort (this "hold everything to the end" variant is Strong Strict 2PL, what production engines implement). No new locks after first release — that ordering is what guarantees serializability.
Holding write locks to commit is also what gives you no-dirty-reads/writes at Read Committed Isolation; 2PL extends the same idea to reads, closing skew and non-repeatable reads by force of blocking.
Predicate Locks and Next-Key Locking
Row locks don't cover rows that don't exist yet — the phantom hole from Write Skew and Phantom Writes. True predicate locks (lock the condition) are theoretically clean but expensive to evaluate, so engines approximate with index-range locks: InnoDB's next-key lock = record lock + gap lock on the index interval. SELECT ... WHERE age BETWEEN 20 AND 30 FOR UPDATE locks the index gaps covering that range, so a concurrent insert of age=25 blocks until commit. Cost: reduced concurrency on hot ranges and the infamous next-key deadlock patterns on unindexed predicates (which degrade toward table-ish locking — index your predicates).
Deadlocks: Detection and Discipline
Any lock-wait cycle (T1 holds A wants B; T2 holds B wants A) deadlocks permanently without intervention. Engines maintain a wait-for graph and poll for cycles (InnoDB checks on every lock wait when innodb_deadlock_detect is on, plus a 50 s innodb_lock_wait_timeout fallback). The victim — usually the txn with fewest rows modified — is aborted with a retryable error (MySQL 1213, PG 40P01).
| Quantity | Typical value | Why it matters |
|---|---|---|
| Detection latency | Immediate–1 s (graph poll) | Deadlocked txns fail fast; lock-wait timeouts (50 s default InnoDB) are the slow path |
| Victim choice | Least-undo / most-recent | App retry cost minimized; still a full-txn retry |
| Prevention discipline | Global lock order + short txns + indexed predicates | 90% of production deadlocks are app-ordering bugs, not engine bugs |
| Hot-row ceiling | ~1–10K updates/s per row (lock + WAL + fsync bound) | Counters/inventory rows need batching, partitioning, or optimistic techniques beyond this |
Rules to recite: acquire locks in a consistent global order (e.g. parent→child, ascending PK), keep transactions short (no user think-time inside), touch the fewest rows possible, and index every locked predicate.
Concrete Lock Compatibility
| Held \ Wanted | S (read) | X (write) |
|---|---|---|
| S | Granted | Waits |
| X | Waits | Waits |
Upgrades (S→X on the same row, the classic read-then-write) are a deadlock magnet: two txns both holding S that both want X deadlock deterministically. Prefer SELECT FOR UPDATE (takes X/IX upfront) for read-modify-write.
Trade-offs
| Approach | Pros | Cons | Use when |
|---|---|---|---|
| 2PL (pessimistic) | True serializability; intuitive "blocked = safe"; no abort storms at low contention | Lock waits inflate p99; deadlocks; hot-row collapse; gap locks surprise | Contended OLTP with strict invariants, InnoDB SERIALIZABLE, legacy RDBMS shops |
| SSI (optimistic) | Readers never block; aborts only on real danger patterns | Abort/retry load; false positives | Mostly-read mixes with occasional strict txns — see Serializable Snapshot Isolation |
| Serial execution | No locks at all | Partition + sproc discipline | Partitionable OLTP — see Serial Execution and Achieving ACID |
| Snapshot + app locks | Cheap, targeted | Manual, phantom-prone | Single hot-row cases (FOR UPDATE, materialized lock rows) |
Interview One-Liners
- Q: 2PL in one sentence? A: Take shared/exclusive locks while running, release nothing until commit — any conflicting txn waits, which forces an equivalent serial order.
- Q: Why must locks be held to commit, not released early? A: Early release lets another txn see your uncommitted state or overwrite it before you commit — reintroducing dirty reads/writes and breaking the serial order.
- Q: How does 2PL stop phantoms? A: Range/gap (next-key) locks on the index interval cover not-yet-existing rows, blocking concurrent inserts into the locked predicate range.
- Q: What causes most production deadlocks? A: Inconsistent lock acquisition order across code paths plus long transactions — fix by ordering locks globally and shortening txns.
- Q: S→X upgrade deadlock — what and how to avoid?
A: Two txns holding shared locks both requesting exclusive deadlock; avoid with
SELECT FOR UPDATEtaking the exclusive lock upfront. - Q: 2PL vs SSI in one line? A: 2PL prevents conflicts by blocking (latency, deadlocks); SSI detects dangerous patterns and aborts (retries, no blocking) — pessimistic vs optimistic serializability.