Lesson 19 · Transactions & concurrency
Locks & deadlocks
MVCC means reads don't lock — but writes do. What can go wrong, and why.
Your win: explain what locks writes take, what SELECT … FOR
UPDATE is for, and how a deadlock forms and gets resolved.
Reads don't lock; writes do
The MVCC payoff (Lesson 6): a plain SELECT takes no row locks
— it reads a snapshot. But writes lock: an UPDATE/DELETE
takes a row-level lock, so two writers to the same row
serialize (the second waits for the first to commit).1
SELECT … FOR UPDATE
Sometimes you read a row intending to update it, and must stop anyone else changing it in
between. SELECT … FOR UPDATE locks the matched rows now, so a concurrent
writer blocks until you commit. FOR SHARE is the weaker "don't let it change,
but others may also share-read" variant. Table-level locks (from DDL / LOCK
TABLE) exist but are rarer.
Deadlocks: a cycle of waits
Neither can proceed. Postgres's deadlock detector spots the cycle, picks a victim, and aborts it with a deadlock error; the other transaction continues. Your app retries the victim.2
WithUpdateLock() / WithShareLock() that append
FOR UPDATE / FOR SHARE to a query
(internal/golibs/database/db.go:136). For coordination
outside row locking, there are advisory locks —
pg_try_advisory_lock(hashtext($1))
(internal/golibs/database/pg_advisory_lock.go) — a named,
app-defined lock (e.g. "only one worker runs this job at a time").
PostgreSQL — Explicit Locking
Row- and table-level lock modes, FOR UPDATE/FOR SHARE,
advisory locks, and how the deadlock detector works.
Check yourself (from memory)
Q1. Under MVCC, a plain SELECT…
Q2. SELECT … FOR UPDATE is used to…
Q3. When Postgres detects a deadlock it…
SELECT … FOR UPDATE
explicitly locks rows you're about to change (FOR SHARE is weaker);
table-level locks are rarer. A DEADLOCK is a cycle — A holds lock 1 wanting 2, B holds 2
wanting 1. Postgres's deadlock detector aborts one (the victim) with a deadlock error so
the other proceeds; the app retries. Prevent by acquiring locks in a consistent order.
Our repo has WithUpdateLock()/WithShareLock() and advisory locks
(pg_try_advisory_lock).NOWAIT /
SKIP LOCKED for queue-style workers? Ask me.