Last modified: May 06, 2025
This article is written in: πΊπΈ
TwoβPhase Locking (2PL)
TwoβPhase Locking (2PL) is a scheduling rule built into database engines to keep concurrent transactions from stepping on each other. 2PL does not change what your application writesβit changes when each transaction is allowed to read or write shared data so that the overall result is the same as some serial order.
Every transaction first grows its set of locks, hits a lockβpoint, and only then starts giving locks back. Once it starts giving locks back it may never take another one.
RealβWorld Analogy:
βββ Growing Phase ββββββ βββ Shrinking Phase βββ
| Collect all library | | Start returning |
| books you need. | ===βΊ | books; you cannot |
| No returns allowed | | borrow more. |
ββββββββββββββββββββββββ βββββββββββββββββββββββ
While you hold a book, nobody else can annotate it. Once you drop it back, anyone may pick it upβbut you may not take another.
After reading you should be able to answerβ¦
- What is TwoβPhase Locking (2PL) and what are its two phases?
- How does 2PL guarantee serializability among concurrent transactions?
- What extra rules do Strict, Rigorous, and Conservative 2PL add and why?
- Which parts are handled automatically by the database engine, and which must the application developer code explicitly?
- Show a concrete transferβfunds example that follows 2PL.
Overview
Before diving into lock types and variations, it helps to see where 2-phase locking draws the line between taking locks and releasing them. The timeline below exaggerates every step so the lock-point is unmistakable.
#
βββββββββββββββββββββββββββββββ Growing Phase ββββββββββββββββββββββββββββββββ βββββββββββββ Shrinking Phase ββββββββββββ
Timeline βΊ β S(A) β X(B) β X(C) β S(D) β X(E) β β ----β---- β rel S(A) β rel X(B) β β¦ β rel X(E) β
ββββββββββ΄βββββββββ΄βββββββββ΄βββββββββ΄βββββββββ΄ββ lock-point ββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββββββ
β²
βββ no new locks may be taken past this point
Legend: S = shared/read lockβ X = exclusive/write lock
Your application decides which rows or tables to lock and when the transaction starts and ends. The database engine enforces the arrows: once the transactionβs first lock is released it may only releaseβnever again acquireβfurther locks.
Who Does What? (Engine vs Application)
The clean handβoff between your code and the engine is what makes twoβphase locking practical. Think of it like a film crew:
- Your application is the directorβit decides the story: which rows/tables to touch and when the scene (transaction) starts and ends.
- The database engine is the stage managerβit controls access to the set so no actor bumps into another midβscene.
βββββββββββββββββββββββββ BEGIN / COMMIT / ROLLBACK
β Application Code β ββββββββββββββββββββββββββββββββββββΆ starts & ends txn
βββββββββββββββββββββββββ (defines scope)
β² SQL stmts / lock hints β
β βΌ
βββββββββββββββββββββββββ grants / blocks ββββββββββββββββββββββββββββ
β DB Engine Scheduler ββββββββββββββββββββββββββββββ Lock Manager (2PL) β
β (2PL enforcer) β ββββββββββββββββββββββββββββ
βββββββββββββββββββββββββ protects data while letting others run
What needs to happen? | Handled inside the engine | What you still write |
Get & hold the right locks | Automatic per statement and current isolation level | Optionally request extras (SELECT β¦ FOR UPDATE , LOCK TABLE β¦ ) |
Detect / resolve deadlocks | Waitβfor graph, timeouts, victim selection | Decide retry/backβoff strategy; set lock_timeout if offered |
Mark txn start / finish | β | BEGIN , COMMIT , ROLLBACK |
Pick isolation level rules | β (engine just applies them) | SET TRANSACTION ISOLATION LEVEL β¦ |
Choose lock granularity | Engine picks row / page / table automatically | Provide hints via DDL or options (ROWLOCK , NOLOCK ) |
Rule of thumb: your code says when a transaction runs and what it does; the engine decides how to guard the data while it happens.
The Two Phases of 2PL
During the growing phase the engine takes every lock the transaction asks for. The instant the transaction releases its first lock it has crossed the lockβpoint and entered the shrinking phase; from that moment no new locks are permitted.
time βΊ ββ¬βββ acquire S(A) ββ acquire X(B) βββ¬β commit ββΆ
β (growing) β (shrinking)
β β
lockβpoint βββββββββββββββββββββββββββββ
Why it works: if every transaction follows that pattern, their critical sections never overlap in a way that produces a nonβserial schedule.
Variations of TwoβPhase Locking
Strict 2PL (default in PostgreSQL, MySQLβInnoDB, SQL Server)
Keep X locks to the very end, release S locks earlier. Default in PostgreSQL, MySQLβInnoDB, SQL Server
time βΊ ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
growing phase shrinking phase
Row A S: ββββββββββββ release
Row A X: βββββββββββββββββββββββββββββββββββββ
ββ COMMIT ββΊ drop X locks
Row B S: ββββββββ release β
Row B X: βββββββββββββββββββββββββββββββββββββ
Prevents dirty reads & cascading aborts while still letting readβonly transactions slip past once they no longer conflict.
Rigorous 2PL
Hold all locks (shared & exclusive) until end of transaction.
time βΊ ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Row A S: ββββββββββββββββββββββββββββββββββββββββββββββββββ
Row A X: ββββββββββββββββββββββββββββββββββββββββ
Row B S: βββββββββββββββββββββββββββββββββββββββββββββ COMMIT ββΊ drop every lock
Row B X: βββββββββββββββββββββββββββββββββ
Simplest to reason about and fully recoverable, but worst concurrency: even read locks block everybody else until the very end.
Conservative (Static) 2PL
Grab every lock you will ever need before doing any work. If a lock is unavailable, wait. Deadlockβfree at the cost of longer initial waits.
time βΊ ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
tryβlock {A,B,C} ββ’ acquired ββ¬ββββββββββββββ work (reads/writes) ββββββββββββββ¬ββ COMMIT ββΊ release all
Row A X: ββββββββββββββββββββββββββββββββββββββββββ
Row B X: ββββββββββββββββββββββββββββββββββββββββββ
Row C S: ββββββββββββββββββββββββββββββββββββββββββ
Because the transaction first waits until it can lock every object it will ever touch, no cycle of waitβfor edges can formβhence no deadlocks. The tradeβoff is potential underβutilisation while the big lock request is waiting.
Concrete Example β Funds Transfer
Below is everything you write (application layer) versus what the engine does silently.
-- application code ---------------------------------------------
BEGIN TRANSACTION; -- start growing phase
SELECT balance -- engine: Sβlock row A
FROM Accounts
WHERE id = 'A'
FOR UPDATE; -- engine upgrades to Xβlock row A
SELECT balance -- engine: Sβlock row B
FROM Accounts
WHERE id = 'B'
FOR UPDATE; -- engine upgrades to Xβlock row B
UPDATE Accounts SET balance = balance - 100 WHERE id = 'A';
UPDATE Accounts SET balance = balance + 100 WHERE id = 'B';
COMMIT; -- locks released automatically (strict 2PL)
Under the hood (engine):
- Acquires rowβlevel locks on
A
andB
in exclusive mode (growing phase). - At
COMMIT
it flushes the log, marks the txn committed, then releases the locks (shrinking phase).
How 2PL Ensures Serializability
Imagine two transactions T1 and T2 that both read and write the same rows. Because each must hold a conflicting lock before proceeding, either T1 obtains the lock first (T2 waits) or T2 obtains the lock first (T1 waits). The executed order is therefore serialβeven though the waits happen inside one schedule.
Challenges & Remedies
Challenge | Why it happens | Common remedies |
Deadlock | T1 holds A wants B; T2 holds B wants A | β Lock ordering, β‘ short transactions, β’ automatic deadlock detection + retry |
Reduced concurrency | Locks block readers/writers | Choose proper isolation level (e.g. Snapshot/MVCC where possible), finerβgrained locks |
Lock management overhead | Highβthroughput workloads | Batch writes, keep transactions lean, use multiversion techniques |
Deadlock Illustration:
Waitβfor graph
T1 ββββΊ T2
β² β
βββββββββ (cycle β deadlock)
Best Practices When Coding with 2PL
- Keep transactions small and quick.
- Access objects in a consistent order (e.g. alphabetical by primary key).
- Use
SELECT β¦ FOR UPDATE
only when you truly need exclusive access. - Prefer rowβlevel locks over table locks for write heavy systems.
- Monitor blocked/locking sessions (
pg_stat_activity
,INFORMATION_SCHEMA.INNODB_TRX
, etc.).
Further Reading
- ANSI/ISO SQL Standard β isolation levels & locking semantics
- Bernstein & Newcomer, Principles of Transaction Processing
- PostgreSQL docs β Explicit Locking, Concurrency Control
- Fekete etβ―al., "Making Snapshot Isolation Serializable" (SIGMOD 2005)