skip to content

Locking & Two-Phase Locking

The lock-based half of concurrency control: the two-phase locking protocol that guarantees serializable schedules, the lattice of lock modes and granularities engines expose, and what happens when lock waits form a cycle. Interviewers test whether I can reason about who blocks whom and why.

part ofRelational database conceptsoverview, primer and where to startread it →
on this pageshow

questions

16

Two database transactions are each waiting for a lock the other already holds, and neither can make progress. What is this situation called, how does a relational database typically get out of it, and what is the application expected to do?

level: juniorimportance: must knowfreq 66%

answer

  1. cycle in the wait-for graph
  2. engine picks a victim, full rollback
  3. distinct retryable error code
  4. retry the whole transaction, with jitter
  5. usual cause: inconsistent lock order

basics

~20 s

It is a deadlock. The database detects the cycle of waits, picks one transaction as victim, and rolls it back with a deadlock error; the survivor proceeds. The application must catch that error and retry the whole transaction, ideally after a short backoff.

solid answer

~50 s

That is a **deadlock**: a cycle in the wait-for graph, where each transaction waits on a lock held by another. Nobody can proceed and no amount of waiting resolves it, so the database must break the cycle itself. Most engines run a deadlock detector that periodically builds the wait-for graph (a node per transaction, an edge per "waits for") and looks for a cycle. On finding one it selects a **victim** — often the transaction with the least work done or the fewest locks held — rolls it back, and returns a specific deadlock error. The other transactions immediately acquire their locks and continue. Some systems instead rely on a lock-wait timeout, which resolves the same situation more bluntly and more slowly. The application must treat the deadlock error as **transient and retryable**: catch it at the transaction boundary and re-run the entire unit of work, since the victim's changes were fully rolled back. Repeated deadlocks are a design signal — inconsistent lock ordering or long transactions.

code

text · 5 lines
text
T1: locks row A ......... waits for row B
T2: locks row B ......... waits for row A

wait-for graph:   T1 --> T2 --> T1     (cycle)
detector aborts one of them; the other proceeds

go deeper

for a junior

State the definition (each transaction waits for a lock the other holds), that the database aborts one with a deadlock error and rolls it back fully, and that the application retries.

for a middle

Add the wait-for graph model, the victim-selection heuristics, the difference from a lock-wait timeout, and the retry contract with bounded attempts and jittered backoff.

for a senior

Treat the deadlock rate as a signal: locate the conflicting access orders from the engine's deadlock reports, fix the ordering or shorten the transactions, and keep retry as a safety net rather than the remedy.

for a principal

Set the platform-wide policy — a shared retry wrapper, idempotent transaction boundaries, deadlock-rate SLOs and alerting — and decide when a hot contention point should be redesigned rather than retried.

## What a deadlock is A transaction that needs a row or object already locked in an incompatible mode waits. Waiting is normal and usually short. A **deadlock** is the pathological case: transaction T1 holds lock A and wants lock B; transaction T2 holds lock B and wants lock A. Each is waiting for something only the other can release, and neither will release anything before it finishes. This is a *cycle*, and it is permanent — waiting longer cannot help, so something external must break it. The standard model is the **wait-for graph**: one node per active transaction, and a directed edge T1 → T2 whenever T1 is blocked on a lock T2 holds. A deadlock is exactly a cycle in that graph. The cycle may involve two transactions or twenty; the two-transaction case is simply the common one. ## The everyday cause Overwhelmingly, deadlocks come from **inconsistent access order**. One code path updates account 1 then account 2; another updates account 2 then account 1. Run them concurrently and each grabs its first row before the other asks for it, and the cycle is complete. The same thing happens implicitly: two batch jobs updating the same set of rows in whatever order their result sets happened to arrive, or a transaction that reads a row and only later upgrades to a write on it while another does the same in the other direction. Note that a deadlock is not a bug in the database and not a sign of corruption. It is the concurrency-control mechanism doing its job: the alternative to detecting and breaking the cycle is two sessions hung forever. ## How the database resolves it Two strategies exist, and most systems use one of them as the primary mechanism. **Detection.** The engine builds or maintains the wait-for graph and searches for cycles — typically not on every wait (too expensive) but after a transaction has waited past a short threshold, or on a periodic sweep. On finding a cycle it must break it by aborting a member. The chosen transaction is the **victim**: it is rolled back completely, all its locks are released, the cycle disappears, and the remaining transactions immediately proceed. The victim's session receives a distinct error code identifying the failure as a deadlock. **Timeout.** Simpler systems (and some configurations of sophisticated ones) skip graph analysis: any transaction that waits longer than a configured lock-wait timeout is aborted. This resolves genuine deadlocks eventually, but it cannot distinguish a deadlock from a merely slow lock holder, so it also kills innocent waiters, and it makes every deadlock cost at least the full timeout in latency. ## Choosing the victim Engines aim to abort the transaction that is cheapest to redo, using heuristics such as: how much work it has done (rows changed, log records written), how many locks it holds, how long it has been running, and sometimes an operator-assigned priority. Because the criteria are internal, the application cannot assume it will or will not be the victim — either side of the cycle can lose, so *every* transaction that takes locks needs the same retry handling. ## What the application must do The victim's transaction is **entirely rolled back** — this is the key fact for a junior to internalise. Nothing it wrote survives, so there is no partial state to clean up and no half-finished work to reconcile. The correct handling is: 1. Catch the deadlock error specifically, distinguishing it from constraint violations and other non-retryable errors. 2. Retry the **whole transaction** from the start, re-reading everything, because the surviving transaction has since changed the data. 3. Bound the retries — a handful of attempts — and back off with a small randomised delay, so the retried transactions do not immediately collide again. 4. Keep non-transactional side effects (emails, payment calls, messages) outside the retried block, since the block may execute more than once. 5. Count deadlocks as a metric. A trickle is normal in a busy write-heavy system; a rising rate is a design problem to fix, not a retry budget to increase. ## What it is not A deadlock is not the same as a lock-wait timeout: the timeout means somebody held a lock too long, with no cycle involved, and the fix is usually the slow holder, not the ordering. It is not the same as a serialization failure under snapshot or serializable isolation, which involves no waiting cycle at all and is caused by conflicting reads and writes rather than by lock ordering. And it is not livelock — repeated aborts and retries that never make progress — which is what unbounded, unjittered retry loops turn a deadlock into.

  • After the database rolls back the victim, is there any partial state the application needs to clean up?
    No. The rollback is complete — every row the victim changed reverts, and every lock it held is released. That is what makes a plain retry of the whole transaction correct. The only things that survive are effects outside the database, such as emails already sent or external API calls already made, which is why those belong after commit rather than inside the transaction.
  • How is a deadlock different from a lock-wait timeout?
    A deadlock is a cycle: the waiting transactions can never make progress, so waiting longer is futile and the engine breaks the cycle by aborting a victim. A lock-wait timeout means a single transaction waited too long on a holder that may still be working normally, with no cycle involved. The fixes differ: deadlocks point at inconsistent access order, timeouts usually point at a transaction holding locks too long.
  • Can more than two transactions be involved in one deadlock?
    Yes. Any cycle in the wait-for graph is a deadlock, so three or more transactions can form a ring where each waits on the next. The detector finds the cycle regardless of its length and aborts one member, which is enough to break the ring and let the remaining transactions proceed.

Two people meet in a narrow corridor, each holding the door the other needs to pass. No amount of politeness resolves it; someone has to step all the way back to the start of the corridor and try again.

saying these in an interview costs you the question

  • Believing a deadlock resolves itself if you just wait longer
  • Thinking the victim's work is partially applied and needs manual cleanup
  • Retrying only the failed statement instead of the whole transaction
  • Assuming your transaction will never be chosen as the victim
  • Confusing a deadlock with a slow query or a lock-wait timeout

context

open as a page

In the two-phase locking (2PL) concurrency-control protocol used by relational database engines, what are the two phases, what rule must a transaction obey in each, and what is the transaction's "lock point"?

level: juniorimportance: must knowfreq 55%

basics

~20 s

Growing phase: the transaction may take locks but release none. Shrinking phase: it may release locks but take no new ones. The lock point is the instant between them, where it holds every lock it will ever hold.

open as a page

In a relational database engine, what is the difference between a shared lock and an exclusive lock, and which operations acquire each one?

level: juniorimportance: must knowfreq 72%

basics

~20 s

A shared (S) lock lets other shared locks in but blocks exclusive ones — many readers, no writer. An exclusive (X) lock excludes everything, readers and writers alike. Writes take X on the rows they modify; reads take S only in lock-based (non-MVCC) reading.

open as a page

A database engine can take locks at row, page, or whole-table granularity. What are the trade-offs, and what makes an engine choose a coarser or finer level?

level: middleimportance: must knowfreq 58%

basics

~20 s

Fine granularity (row) maximises concurrency but costs memory and CPU per lock; coarse granularity (table) is nearly free to track but serialises unrelated work. Engines choose by how many rows a statement touches — few rows, lock rows; whole table, lock the table.

open as a page

What does `SELECT ... FOR UPDATE` give you that a plain `SELECT` does not, and how does `FOR SHARE` differ from it?

level: middleimportance: must knowfreq 64%

basics

~20 s

FOR UPDATE takes exclusive row locks on the rows it reads, held until commit, so no one else can modify or lock them — it turns a read into a claim. FOR SHARE takes shared row locks: others may still read, but nobody may modify the rows until you commit.

open as a page

A write-heavy OLTP service reports a rising number of database deadlock errors under load, and simply retrying them is no longer enough. What design and access-pattern changes reduce how often deadlocks occur in the first place?

level: seniorimportance: must knowfreq 48%

basics

~20 s

Make every code path touch shared rows in the same order (sort keys before batch writes), keep transactions short and free of external waits, take the strongest lock you need up front instead of upgrading later, and reduce contention on hot rows by splitting or aggregating them.

open as a page

A service intermittently fails with the database's deadlock error, which rolls the transaction back entirely. Design the application-side handling for that error: where the retry lives, how many attempts, what must not be inside the retried block, and how you distinguish it from errors that must not be retried.

level: seniorimportance: must knowfreq 56%

basics

~20 s

Wrap the whole transaction in a bounded retry — about three to five attempts with exponential backoff and jitter — that re-reads and re-decides each time. Catch only the deadlock and serialization error classes; never retry constraint or business errors. Keep external side effects outside the retried block, and emit metrics.

open as a page

Contrast lock-based concurrency control using two-phase locking with multi-version concurrency control (MVCC): when one transaction reads a row and another writes the same row, what happens under each scheme, and what are the tradeoffs?

level: seniorimportance: must knowfreq 50%

basics

~20 s

Under 2PL the reader's shared lock and the writer's exclusive lock conflict, so one blocks. Under MVCC the writer creates a new version and the reader sees an older snapshot, so readers never block writers. MVCC costs version storage and cleanup, and snapshot isolation alone is not serializable.

open as a page

A relational engine can resolve stuck lock waits either by periodically searching a wait-for graph for cycles or by aborting any transaction that has waited longer than a configured lock-wait timeout. Compare the two approaches, and explain what a deadlock detector considers when choosing which transaction to abort.

level: middleimportance: should knowfreq 45%

basics

~20 s

Graph detection finds real cycles quickly and aborts only a true participant; timeouts are cheap but slow and kill innocent waiters that were merely blocked by a slow holder. Victims are picked to minimise wasted work — fewest changes, fewest locks, shortest run, lowest priority.

open as a page

What does it mean for a schedule of concurrent database transactions to be conflict-serializable, and why does obeying the two-phase locking rule (acquire all locks before releasing any) guarantee that property?

level: middleimportance: should knowfreq 45%

basics

~20 s

Conflict-serializable means the schedule can be turned into some serial order by swapping only non-conflicting operations — equivalently, its precedence graph has no cycle. Two-phase locking guarantees it because ordering transactions by lock point is always a valid serial order.

open as a page

Compare basic two-phase locking with strict 2PL and with rigorous (strong strict) 2PL: in each variant, which locks are held until the transaction commits, and what does a database engine gain by choosing a stricter variant?

level: middleimportance: should knowfreq 40%

basics

~20 s

Basic 2PL may release any lock once it stops acquiring. Strict 2PL holds all exclusive (write) locks to commit or abort. Rigorous 2PL holds all locks, shared and exclusive, to commit. Stricter buys recoverability, no cascading aborts, and commit-order serialization — at the cost of concurrency.

open as a page

What is a cascading abort in a database, how can releasing exclusive (write) locks before commit under basic two-phase locking cause one, and how does holding those locks until commit eliminate the problem?

level: seniorimportance: should knowfreq 38%

basics

~20 s

A cascading abort is one transaction's rollback forcing others to roll back because they read its uncommitted writes. Releasing write locks early lets that dirty read happen. Holding exclusive locks to commit means nobody ever reads uncommitted data, so no cascade is possible.

open as a page

Some database engines take intent locks — modes named IS, IX and SIX — on a table before locking individual rows inside it. What problem do intent locks solve, and how does SIX differ from IX?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Intent locks announce at the table level that finer locks exist below, so a transaction wanting the whole table can detect a conflict in one check instead of scanning every row lock. IS means shared locks below, IX exclusive locks below, SIX means shared on the whole table plus exclusive locks on some rows.

open as a page

What is lock escalation in a database engine, and how would you recognise and mitigate it when a production workload starts suffering from it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Lock escalation is the engine trading many fine-grained row or page locks for one coarse table lock once a transaction crosses a lock-count or memory threshold. It saves lock-manager memory but suddenly serialises everyone else on the table. Fix it by making statements touch fewer rows and by batching.

open as a page

Beyond shared and exclusive, some engines offer an 'update' (U) lock mode. What is it for, and what would go wrong without it?

level: seniorimportance: nice to knowfreq 24%

basics

~20 s

An update (U) lock means "reading now, probably writing next". It is compatible with shared locks but not with other update or exclusive locks, so only one prospective writer can be scanning at a time — which prevents two readers from both trying to upgrade S to X and deadlocking.

open as a page

On a database engine using strict two-phase locking, throughput on a contended workload rises as you add concurrent transactions and then collapses past a certain point. Explain the mechanism behind that collapse, and how you would design and operate the system to stay on the healthy side of the curve.

level: principalimportance: nice to knowfreq 25%

basics

~20 s

Blocked transactions keep the locks they already hold, so each new conflict creates more blocking — lock thrashing. Fix it by shortening lock hold time (short transactions, no remote calls inside them), reducing hot-row conflict, and capping concurrent writers with admission control rather than raising the pool.

open as a page