Skip to main content
Back to module
Database Administration · Performance

Transactions and locking

Reading25mLesson 3 of 3 in this module

What you'll learn

  • Explain isolation levels and deadlocks
  • Describe the four ACID guarantees of a transaction
  • Match dirty, non-repeatable and phantom reads to the isolation levels that prevent them
  • Prevent lost updates with row locks or atomic updates

Explain ACID, the four isolation levels and their anomalies, how row locks work and how databases detect and break deadlocks.

Why it matters

A database serves many users at once. Two people buy the last ticket, two jobs update the same account balance, a report reads a table while it is being changed. Transactions and locks decide what each of them sees and who waits. When they are misunderstood you get lost updates, impossible report totals, lock pile-ups and deadlock errors, which are some of the hardest production incidents to explain.

Transactions and ACID

A transaction groups statements into one unit:

SQL
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;   -- or ROLLBACK; to undo both

ACID describes the guarantees:

  • Atomicity: all statements take effect, or none do.
  • Consistency: constraints hold before and after.
  • Isolation: concurrent transactions do not see each other's half-finished work (to a configurable degree).
  • Durability: once committed, the change survives a crash (it has been written to the write-ahead log).

Isolation levels and anomalies

Full isolation is expensive, so SQL defines levels that allow some anomalies:

  • Dirty read: you see another transaction's uncommitted change, which may be rolled back.
  • Non-repeatable read: you read a row twice in one transaction and get different values, because someone committed in between.
  • Phantom read: you run the same WHERE twice and new rows appear.
  • Lost update / write skew: two transactions read, then both write based on what they read, and one effect is lost or a rule across rows is broken.
LevelDirty readNon-repeatable readPhantom
Read uncommittedpossiblepossiblepossible
Read committedpreventedpossiblepossible
Repeatable readpreventedpreventedpossible in the standard
Serializablepreventedpreventedprevented

Real engines differ from the table. PostgreSQL defaults to read committed, treats read uncommitted as read committed, and its repeatable read (snapshot isolation) also prevents phantoms; serializable adds checks that abort one transaction with SQLSTATE 40001 if the result could not have happened serially. MySQL InnoDB defaults to repeatable read. SQL Server defaults to read committed with locking unless snapshot options are enabled. Always check the documentation for your engine and version.

How it works: MVCC and locks

PostgreSQL, MySQL InnoDB and Oracle use MVCC (multi-version concurrency control): an update writes a new version of the row, and each transaction reads the versions visible to its snapshot. The big benefit is that readers do not block writers and writers do not block readers.

Writers still block other writers on the same row. When transaction A updates row 1, transaction B's update of row 1 waits until A commits or rolls back. You can take the lock deliberately when you read:

SQL
BEGIN;
SELECT stock FROM tickets WHERE id = 7 FOR UPDATE;  -- lock the row
-- check stock > 0 in the app, then:
UPDATE tickets SET stock = stock - 1 WHERE id = 7;
COMMIT;

Without FOR UPDATE (or a single atomic UPDATE ... SET stock = stock - 1 WHERE id = 7 AND stock > 0), two buyers at read committed can both read stock = 1 and both sell the last ticket. Schema changes take stronger table-level locks; that is covered in the migrations lesson.

Deadlocks

A deadlock happens when two transactions each hold a lock the other needs:

T1: UPDATE accounts ... WHERE id = 1;   -- T1 locks row 1
T2: UPDATE accounts ... WHERE id = 2;   -- T2 locks row 2
T1: UPDATE accounts ... WHERE id = 2;   -- T1 waits for T2
T2: UPDATE accounts ... WHERE id = 1;   -- T2 waits for T1: deadlock

Neither can proceed, so the database detects the cycle and aborts one of them. PostgreSQL checks after deadlock_timeout (1 second by default) and raises SQLSTATE 40P01; MySQL reports error 1213. The survivor continues normally.

How to reduce deadlocks:

  • Lock in a consistent order everywhere, for example always update the lower account id first.
  • Keep transactions short: no user think-time or network calls inside BEGIN ... COMMIT.
  • Index the columns used in UPDATE ... WHERE so fewer rows are locked.
  • Retry the aborted transaction in application code. Deadlocks and serialization failures are expected events, not bugs, when retried safely.

What a DBA watches

Long-running and "idle in transaction" sessions hold locks and stop cleanup. In PostgreSQL, pg_stat_activity shows them and pg_locks with pg_blocking_pids(pid) shows who blocks whom. Settings such as idle_in_transaction_session_timeout, lock_timeout and statement_timeout put limits on the damage.

Common mistakes

  • Assuming the default isolation level is serializable.
  • Read-then-write logic without a lock or an atomic update (lost updates).
  • Long transactions that wrap slow work, holding locks for minutes.
  • Not retrying on 40001 / 40P01.

Quick recap

  • Transactions give ACID; isolation levels trade anomalies for concurrency, and engines implement them differently.
  • MVCC lets readers and writers avoid blocking each other; writers on the same row still queue.
  • Deadlocks are detected and one transaction is aborted. Consistent lock order, short transactions and retries keep them rare and harmless.

Try it

The Do tab quiz walks through isolation and deadlock scenarios.

Key terms (6)

ACID
Atomicity, consistency, isolation and durability: the guarantees a database transaction provides.
Dirty read
Seeing another transaction's uncommitted change, which may later be rolled back.
Phantom read
Running the same WHERE twice in one transaction and finding new rows the second time.
MVCC
Multi-version concurrency control: updates write new row versions so readers and writers do not block each other.
deadlock
Two transactions each holding a lock the other needs; the database detects the cycle and aborts one of them.
Lost update
Two transactions read the same value and both write based on it, so one change is silently overwritten.

Recap

  • Transactions are ACID; isolation levels trade anomalies for concurrency, and engines implement them differently.
  • PostgreSQL defaults to read committed, MySQL InnoDB to repeatable read.
  • MVCC lets readers and writers avoid blocking each other, but writers on the same row still queue.
  • Use SELECT ... FOR UPDATE or an atomic UPDATE to prevent lost updates.
  • Lock in a consistent order, keep transactions short and retry aborted ones.
Also in this lesson: