Skip to main content

Concurrency Control: MVCC, Locking, OCC

Level 2: Level 2: Data Storage & Modelinghard30 minmvcclockingconcurrency

MVCC versions rows so readers and writers never block each other (at vacuum/bloat cost); go optimistic under low contention, pessimistic under high, and shard hot counters.

The contract vs the machinery

Isolation levels are the contract; concurrency control is how the database actually delivers them. There are two big families, and modern databases lean on the first.

MVCC: readers don't block writers

Multi-Version Concurrency Control (MVCC) is the reason "readers don't block writers and writers don't block readers" in Postgres, MySQL InnoDB, Oracle, and most serious OLTP engines. The idea: a write does not overwrite a row in place; it creates a new version of the row, tagged with the transaction that created it. Every transaction runs against a consistent snapshot defined by which versions were committed as of its start. So a long analytical read sees a frozen, coherent view while writers keep creating new versions alongside it, and neither waits on the other. This is what makes snapshot isolation cheap and is why read-heavy systems love it.

Check yourself
An analytics connection opens a transaction and then sits idle for an hour while writers keep updating the table. MVCC means readers do not block writers, so is the idle transaction harmless?

The cost of MVCC is that old row versions pile up and must be reclaimed. In Postgres this is VACUUM (and autovacuum); in InnoDB it is the purge thread cleaning the undo log. The dangerous failure mode is a long-running transaction: it holds an old snapshot, so the database cannot reclaim any version newer than that snapshot's start, and dead tuples accumulate as bloat. A single forgotten transaction (an idle-in-transaction connection, a stuck analytics query) can bloat a table many times its live size and tank performance. This is the operational tax of MVCC, and interviewers love it.

Interview nuance: "What breaks if a transaction stays open for an hour?" The strong answer: it pins the vacuum horizon, so dead tuples cannot be reclaimed, the table and its indexes bloat, and sequential scans slow down. Mitigation: idle_in_transaction_session_timeout, keep transactions short, and monitor the oldest transaction age.

Locking and optimistic control

The second family is locking-based / pessimistic concurrency, classically two-phase locking (2PL): acquire shared (read) or exclusive (write) locks, hold them, and release only at commit. It is correct but writers block conflicting readers and each other, so throughput drops under contention, and it introduces deadlocks: transaction 1 holds lock A and wants B, transaction 2 holds B and wants A. Databases handle this by detecting the cycle and aborting one victim, so your app must catch the deadlock error and retry. You reduce deadlocks by acquiring locks in a consistent order (always lock the lower account id first) and keeping transactions short.

Optimistic concurrency control (OCC) assumes conflicts are rare: do not lock, just read a version, and at commit check WHERE version = :read_version; if it changed, abort and retry. OCC wins when contention is genuinely low, because it skips all lock overhead. It loses badly under high contention, because the abort-and-retry rate explodes and you burn CPU redoing work. So the rule is: optimistic under low contention, pessimistic under high contention.

Check yourself
A viral post's like counter takes thousands of increments per second, all on one row. Contention could not be higher. Applying the rule you just read, is a pessimistic row lock the right design?

The hot key

For a hot key specifically (a viral post's like counter taking thousands of increments per second on one row), the wrong move is heavy pessimistic locking, which serializes every writer behind one lock and caps throughput at one-at-a-time. The right moves: shard the counter into N sub-rows (like_count_shard_0..N), increment a random shard, and sum on read, which spreads contention N-fold; or aggregate increments in memory/Redis and flush periodically; or use an atomic in-database increment so each write is a single short operation rather than a read-modify-write holding a lock.

MVCC read:  txn snapshot --> sees v2 (committed), ignores v3 (in-flight) -- no wait
Hot counter: 1 row, all writers -- LOCK --> serialized (bad)
             N shards, random pick ------> ~N-way parallel, sum on read (good)

Recap: MVCC makes readers and writers not block each other by versioning rows, at the cost of vacuum and bloat from long transactions; choose optimistic control under low contention and pessimistic under high, and for a hot key shard the counter instead of serializing writers behind one lock.

Check yourself

For each workload, pick the concurrency approach you would defend in a design review.

User profile edits, where two writers hitting the same row is rare
Withdrawals against a specific account row that several concurrent transfers target
A trending video's view counter absorbing thousands of writes per second on one row

Apply

Your turn

The task this lesson builds to.

Design the concurrency-control strategy for a high-contention counter/like feature so reads stay fast under heavy writes.

Think about

  1. How does MVCC let readers see a snapshot without blocking writers?
  2. When do you choose optimistic (version/CAS) over pessimistic locking?
  3. What is the operational cost of MVCC (bloat, vacuum, long transactions)?

Practice

Make it stick

A second problem on the same idea, so it survives past today.

Design the concurrency control for a YouTube-scale view counter where a trending video takes 100,000 view events per second globally across many regions, the displayed count can lag real time by seconds, but the eventual total must be accurate and durable enough to drive creator payouts.

Think about

  1. Can any single-primary relational row absorb 100,000 writes per second, even sharded?
  2. How do you serve a fast approximate count and a billing-grade exact total from the same events?
  3. Where does deduplication (bots, replays) belong in the pipeline?