Transactions And Concurrency
Locking and Two-Phase Locking
Several transactions may hold S on the same
JrCodex·7 min read
Jr Codex DBMS Notes
Level: Advanced Prerequisites: Chapter 2: Anomalies and Isolation Levels Time to complete: ~20 minutes
Table of Contents
- Shared and Exclusive Locks
- Two-Phase Locking
- Strict 2PL
- Lock Granularity
- Predicate and Gap Locks
- Explicit Locking in SQL
- Summary & Next Steps
1. Shared and Exclusive Locks
The Two Modes
─────────────────────────────────────────
SHARED (S) — a read lock
Several transactions may hold S on the same
item simultaneously.
EXCLUSIVE (X) — a write lock
Only one transaction may hold X, and no S may
coexist with it.
─────────────────────────────────────────
The Compatibility Matrix
─────────────────────────────────────────
requested
held S X
────────────────────
S ✓ ✗
X ✗ ✗
Read the whole thing as: readers do not block
readers; everything else blocks.
─────────────────────────────────────────
The Consequence
─────────────────────────────────────────
Under pure locking, a WRITER BLOCKS ALL READERS
of the same row.
On a read-heavy workload this is severe: one slow
update to a popular row stalls every query
touching it.
Chapter 4's MVCC exists to remove exactly this
problem, and is why most modern databases do not
lock for ordinary reads at all.
─────────────────────────────────────────
2. Two-Phase Locking
The Protocol
─────────────────────────────────────────
Every transaction has two phases:
GROWING may ACQUIRE locks, may not release any
SHRINKING may RELEASE locks, may not acquire any
The moment of the first release is the LOCK
POINT. After it, no new lock may be taken.
locks
held │ ╭────────╮
│ ╱ ╲
│ ╱ ╲
│ ╱ ╲
└──┴────────────────┴──── time
growing ↑ shrinking
lock point
─────────────────────────────────────────
Why Two Phases Guarantee Serialisability
─────────────────────────────────────────
The lock points impose a total order on
transactions.
If T1 reaches its lock point before T2, then T1
precedes T2 in the equivalent serial order — for
every conflict between them, because T1 held all
its locks at once.
So the precedence graph (Chapter 1) cannot
contain a cycle. 2PL produces conflict-
serialisable schedules, always.
─────────────────────────────────────────
What It Does NOT Prevent
─────────────────────────────────────────
DEADLOCK. Two transactions in their growing
phase, each waiting for a lock the other holds,
wait forever. Chapter 5.
CASCADING ABORTS. Basic 2PL may release locks
before commit, so another transaction can read
uncommitted data. Section 3 fixes this.
─────────────────────────────────────────
3. Strict 2PL
The Rule
─────────────────────────────────────────
STRICT 2PL: hold ALL EXCLUSIVE locks until
commit or abort.
RIGOROUS 2PL: hold ALL locks — shared and
exclusive — until commit or abort.
locks
held │ ╭─────────────┐
│ ╱ │
│ ╱ │ ← all released at
│ ╱ │ once, at COMMIT
└──┴─────────────────┴──
commit
─────────────────────────────────────────
Why Every Real System Uses It
─────────────────────────────────────────
Nobody can read an uncommitted write, because the
X lock is held until commit.
So schedules are STRICT (Chapter 1, Section 5):
no dirty reads, no cascading aborts, and undo is
trivial — just restore the before image.
The cost is that locks are held longer, which
increases contention. That trade is why keeping
transactions SHORT matters so much (Module 3,
Chapter 6): the duration of your transaction is
the duration other transactions wait.
─────────────────────────────────────────
4. Lock Granularity
The Hierarchy
─────────────────────────────────────────
DATABASE
└── TABLE
└── PAGE
└── ROW
COARSE (table) few locks, low overhead,
terrible concurrency
FINE (row) excellent concurrency, many
locks, high memory and CPU
overhead
─────────────────────────────────────────
INTENTION LOCKS
─────────────────────────────────────────
Problem: to lock a whole table, must the database
check all million row locks first?
Solution: before locking a row, take an INTENTION
lock on the table above it.
IS intention shared — I will take S locks
below
IX intention exclusive — I will take X locks
below
SIX S on this level plus IX below
Now a transaction wanting a table-level X lock
checks ONE table-level entry, sees an IX, and
knows to wait — without examining any rows.
─────────────────────────────────────────
LOCK ESCALATION
─────────────────────────────────────────
When a transaction holds too many row locks, some
databases ESCALATE to a single table lock.
Saves memory. Destroys concurrency — suddenly one
transaction blocks the entire table.
SQL Server does this. PostgreSQL does NOT (it
stores row locks in the rows themselves, so they
cost no separate memory), which is one reason
large updates behave differently across
databases.
Practical mitigation: update in BATCHES rather
than one enormous statement.
─────────────────────────────────────────
5. Predicate and Gap Locks
Row locks cannot prevent phantoms (Chapter 2) — you cannot lock a row that does not exist yet.
The Problem
─────────────────────────────────────────
T1: SELECT * FROM students WHERE city='Pune'
Locks the 3 matching rows.
T2: INSERT a new Pune student.
No conflict — that row was not locked,
because it did not exist.
T1: re-runs the query ──► 4 rows. PHANTOM.
─────────────────────────────────────────
Two Solutions
─────────────────────────────────────────
PREDICATE LOCKS
Lock the CONDITION: "all rows where
city='Pune'", including future ones.
Theoretically clean; expensive to check, since
every insert must be tested against every held
predicate.
NEXT-KEY LOCKING (what InnoDB does)
Lock the index RANGE — the matching entries
plus the GAPS between and around them.
An insert into a locked gap blocks.
Cheap, because it reuses the index structure.
─────────────────────────────────────────
Why This Explains an InnoDB Surprise
─────────────────────────────────────────
UPDATE orders SET status='x' WHERE amount > 100;
With no index on `amount`, InnoDB cannot lock a
range — so it locks essentially EVERY row it
scans, including non-matching ones.
With an index on `amount`, it locks only the
relevant key range.
So a missing index does not merely make the
statement slow; it makes it lock far more than it
should, and blocks unrelated transactions.
Indexing is a CONCURRENCY concern, not only a
performance one.
─────────────────────────────────────────
6. Explicit Locking in SQL
-- Take an X lock on the rows you read, so a check-then-act is safe.
BEGIN;
SELECT balance FROM accounts WHERE id = 'A' FOR UPDATE;
-- no other transaction can read-for-update or modify this row now
UPDATE accounts SET balance = balance - 100 WHERE id = 'A';
COMMIT;
-- FOR SHARE: prevent modification, allow other readers.
SELECT * FROM courses WHERE id = 10 FOR SHARE;
-- Do not wait — fail immediately if locked.
SELECT * FROM accounts WHERE id = 'A' FOR UPDATE NOWAIT;
-- Skip rows that are locked — the queue-worker pattern.
SELECT * FROM jobs WHERE status = 'pending'
ORDER BY created_at LIMIT 1
FOR UPDATE SKIP LOCKED;SKIP LOCKED Deserves Its Own Note
─────────────────────────────────────────
It is the clean way to build a work queue on a
relational database.
Ten workers all run the same statement. Each
takes a DIFFERENT pending job, because each skips
rows already locked by the others. No contention,
no double-processing, no external queue needed.
Before this existed, people built elaborate
claim-with-update schemes. This replaces all of
them with one clause.
─────────────────────────────────────────
Lock Ordering
─────────────────────────────────────────
When a transaction locks several rows, always
lock them in a CONSISTENT ORDER — by primary
key, for instance.
T1 locking A then B, while T2 locks B then A, is
the textbook deadlock (Chapter 5). Consistent
ordering makes it impossible.
─────────────────────────────────────────
7. Summary & Next Steps
Key Takeaways
- Shared locks are compatible with each other and nothing else, so under pure locking a single writer blocks every reader of that row.
- Two-phase locking guarantees serialisability because lock points impose a total order on transactions; strict 2PL additionally holds exclusive locks to commit, eliminating dirty reads and cascading aborts.
- Intention locks let a table-level lock request be evaluated without examining a million row locks.
- Row locks cannot prevent phantoms, so databases lock index ranges instead — which means a missing index causes over-locking, making indexing a concurrency concern.
Concept Check
- Why does strict 2PL make transaction duration a shared cost rather than a private one?
- What problem do intention locks solve, and what would happen without them?
- Why does an unindexed
UPDATE ... WHERE amount > 100lock more rows in InnoDB than an indexed one?
Next Chapter
→ Chapter 4: Multi-Version Concurrency Control
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index | Back to DBMS Index