MVCC and isolation
Use concurrent transaction histories to distinguish snapshots, write conflicts, write skew, and serializable retry obligations.
On this page
The question for today
What you’ll understand
- Explain why version visibility differs from current physical row contents.
- Construct a write-skew history that violates a cross-row invariant.
- Choose a concurrency mechanism and retry boundary for a stated invariant.
Two operators each see another healthy worker and independently take their own worker offline. Every update targets a different row, yet the shared requirement that at least one worker remain available is violated. Multi-version concurrency control can give both transactions a stable view without making their combined decisions safe. Choose isolation by writing down concurrent reads, writes, and invariants.
A snapshot is a visibility rule
MVCC keeps enough version information for a transaction to read the rows visible under its snapshot while other transactions create newer versions. Visibility need not mean the newest tuple. PostgreSQL's Read Committed ordinarily takes a new snapshot for each command, whereas Repeatable Read uses a transaction-level snapshot. A transaction's own writes have special visibility. These rules affect repeated SELECT results.
Readers can avoid many conflicts with writers, but MVCC does not abolish locks, write conflicts, deadlocks, or cleanup. Long snapshots can delay version cleanup and increase storage pressure. An UPDATE also performs concurrency checks and may wait for another updater. Transactional statements do not automatically make a multi-statement read-modify-write sequence atomic. A single conditional update can express some invariants more directly.
Follow a write-skew history
Begin with workers A and B active. Transaction T1 reads both and plans to disable A if B is active. T2 reads both and plans to disable B if A is active. Under a snapshot-isolation style execution, each sees the initial valid state and writes a different row. Neither overwrites the other's row, so ordinary same-row conflict detection need not reject either. The final state violates the invariant even though both transactions obeyed their local rule.
A serial execution could not produce that result if each transaction re-evaluates the rule against the preceding committed state. This is the meaning of serializability: the committed outcome corresponds to some serial order, not necessarily that transactions literally ran one at a time. PostgreSQL's Serializable isolation detects dangerous dependency patterns and can abort transactions. Alternatively, at Read Committed, acquire a FOR UPDATE row lock on one common guard row, then read the predicate in a subsequent command. Every decision path must follow that protocol; a preexisting Repeatable Read snapshot can remain stale after waiting for a lock.
Make retries part of the application contract
A serialization failure means the transaction must be retried from an appropriate beginning with fresh reads. Repeating only its final UPDATE preserves a decision based on an invalid snapshot. Put retryable database work inside a bounded attempt loop with jitter and a deadline. Do not automatically repeat an external email, payment, or irreversible action inside that loop; record an intent transactionally and arrange idempotent downstream processing when external effects are needed.
Calculate retry load before choosing an aggressive policy. Assume independent attempts with constant failure probability only for the simple estimate below; real contention can correlate failures and worsen as retries increase traffic. Track conflict rate, retries per operation, and tail latency. Lock ordering, shorter transactions, and better contention ownership may solve the cause more effectively than adding attempts. Test the actual concurrent history, including deletion and insertion paths that could change the predicate's meaning.
Work it through
Audit the two-worker decision
A and B are both active. Two snapshot transactions each disable their own worker after reading that the other is active. Then estimate retry work if independent serialization failures occur with probability 0.2.
The initial invariant count is 2. Each transaction's private planned state has count 1 and appears valid.
If both disjoint writes commit, the final count is 0; this is write skew, not a lost update to one row.
Under Serializable, handle a possible abort by rerunning the whole decision. A valid serial order allows only the first disabling decision to succeed as written.
For the toy independent retry model, expected attempts are 1/(1−0.2) = 1.25. Three consecutive failures have probability 0.2³ = 0.008, or 0.8%.
A stable snapshot alone does not protect a cross-row availability invariant; serializable execution or a consistent locking protocol addresses the shared decision.
The living notebook
Explore the model
Interactive lab · reliability
Separate an availability count from a transaction proof
Adjust replica count n and unavailable count k to view n−k up replicas and majority floor(n/2)+1. Use this only to inspect counts near an availability boundary. It has no snapshots, lock manager, conflict detection, or transactions; majority availability does not prove linearizability or durability.
- Available replicas
- 4 of 5
- Majority needed
- 3
- Majority available?
- Yes
A replica count and a majority threshold, without a consensus protocol. Playback highlights fixed replica states. Having a majority alone does not establish linearizability, durability or failure independence. Parameter changes pause playback and recompute the model at the current elapsed time. Reset restores both parameters and returns time to zero. Playback starts paused. Adjust either parameter, then choose Play or Step.
Look forA visible count can state an invariant, but preserving it during concurrency needs a database protocol.
Pause & reason
Check your understanding
Your turn
Try it yourself
Three reviewers must retain at least two active reviewers. Starting from three active rows, two concurrent snapshot transactions each deactivate one row after seeing three. Is the invariant preserved? Give a safe coordination strategy.
Take a moment to make a prediction or work it out on paper. Open a hint when you need one.
Give me a hint
Check the combined final count rather than either transaction's private planned count.
Compare with the solution
Each private result has two active reviewers, but both commits leave one, violating the invariant. A serializable transaction with full-transaction retry can prevent the nonserializable outcome. At Read Committed, every deactivation path can first SELECT FOR UPDATE on the same guard row, then count in a subsequent command with a fresh snapshot. Locking only each transaction's own distinct reviewer row does not coordinate this cross-row predicate.
Keep the idea
Take it with you
- MVCC determines version visibility without automatically protecting every business invariant.
- Disjoint writes can still conflict through the facts on which decisions depend.
- Serializable retries must repeat fresh reads and carefully contain external effects.
Read further
Original sources and deeper explanations to follow at your own pace.
Optional progress, saved in this browser when available. Revisit any lesson at any time.
Lesson not marked complete.