Inside MongoDB Transactions: MVCC, Snapshots, and How WiredTiger Avoids Write Locks
Inside MongoDB Transactions: MVCC, Snapshots, and How WiredTiger Avoids Write Locks
When engineers hear “transactions”, they often assume databases rely heavily on locks.
Row locks. Page locks. Table locks.
But MongoDB’s storage engine WiredTiger takes a different approach.
Instead of locking data during reads and writes, WiredTiger relies on a technique widely used in modern databases:
Multi-Version Concurrency Control (MVCC).
MVCC allows multiple transactions to operate on the same data without blocking each other by maintaining multiple versions of records.
But MongoDB’s design goes further.
It combines:
- MVCC for transactional consistency
- snapshot isolation for reads
- lock-free in-memory updates using skip lists
- hazard pointers for safe memory access
Understanding how these pieces fit together requires looking at two different layers of the storage engine:
- Disk-level transactional storage
- In-memory concurrent update structures
The Core Problem
A database must satisfy three competing requirements:
- Concurrency
- Consistency
- Performance
Consider a simple example:
Transaction A:
update balance = 200
Transaction B:
read balance
If B reads while A is updating, several problems may occur:
- dirty reads
- inconsistent reads
- blocked threads
- lock contention
Traditional databases solve this using locks:
lock(row)
update
unlock(row)
But locks introduce a scalability problem:
- threads wait
- CPU cores idle
- lock queues grow
To avoid this, WiredTiger relies on versioned data instead of locks.
Multi-Version Concurrency Control (MVCC)
MVCC means the database does not overwrite data immediately.
Instead, it creates new versions of records.
Each version is associated with a transaction ID (timestamp).
Example:
| Key | Value | Version |
|---|---|---|
| user1 | A | txn 100 |
| user1 | B | txn 105 |
| user1 | C | txn 110 |
A transaction reads the version that was visible at its snapshot timestamp.
This means:
- writers do not block readers
- readers do not block writers
- transactions see a consistent view of the database
How Snapshot Isolation Works
When a transaction begins, WiredTiger assigns it a read timestamp.
This timestamp defines the snapshot of the database that the transaction will see.
Example timeline:
T1 start → snapshot = 100
T2 writes version 110
T1 reads data
T1 does not see version 110, because it happened after the snapshot.
Instead it reads the latest version ≤ 100.
Snapshot Visibility Rule
A version is visible if:
version_timestamp ≤ transaction_snapshot
Otherwise it is invisible.
This rule guarantees that transactions see a stable snapshot of the database.
Where Versions Are Stored
In WiredTiger, record versions are stored in an update chain attached to each key.
Example:
key: 42
Update chain:
value C (txn 120)
value B (txn 110)
value A (txn 100)
The newest version is at the head of the chain.
Readers traverse the chain until they find a version visible to their snapshot.
Version Selection
Example:
Transaction snapshot = 105
Update chain:
120 -> invisible
110 -> invisible
100 -> visible
The transaction returns value A.
No locks are needed.
How Writes Work
Writes create a new update node.
Example:
old value → B (txn 110)
new write → C (txn 120)
Instead of overwriting the old value, WiredTiger adds a new update node to the chain.
This makes updates extremely cheap because:
- disk pages are not modified immediately
- writes only manipulate memory structures
Later, the storage engine merges updates during reconciliation.
Page Reconciliation
Eventually, updates must be persisted to disk.
WiredTiger performs this process called reconciliation.
During reconciliation:
- update chains are merged
- obsolete versions are removed
- a new B+Tree page is written
flowchart LR
Updates --> Reconciliation
Reconciliation --> NewPage
NewPage --> Disk
This process converts multiple in-memory updates into a new immutable disk page.
The Role of Timestamps
WiredTiger transactions rely heavily on timestamps.
Each transaction receives:
- read timestamp
- commit timestamp
- durable timestamp
Example lifecycle:
txn start → read timestamp = 100
updates created
txn commit → commit timestamp = 120
Visibility rules use these timestamps to determine which versions are visible.
Snapshot Reads in WiredTiger
A read operation combines two data sources:
Disk page
+
Update chain
Example:
Disk page:
key 42 → value A
Update chain:
value B (txn 110)
value C (txn 120)
If the transaction snapshot = 115
Visible value = B
Read Flow
sequenceDiagram
participant Client
participant Page
participant UpdateChain
Client->>Page: locate key
Page->>UpdateChain: check updates
UpdateChain->>Client: return visible version
The disk page acts as the base version, while updates represent newer versions.
Why Disk Pages Are Not Updated Directly
Directly modifying disk pages would require:
- write locks
- page latching
- synchronization between threads
Instead WiredTiger follows a copy-on-write model.
Updates accumulate in memory until reconciliation creates a new page.
Benefits:
- fewer disk writes
- higher concurrency
- reduced write amplification
In-Memory Concurrency: Hazard Pointers
The previous sections describe transactional consistency.
But they do not explain how multiple threads safely access memory.
This is where hazard pointers come in.
Hazard pointers solve a different problem:
safe memory reclamation.
When a thread reads a page:
thread.hazard_pointer = page
This signals:
“Do not free this memory while I’m using it.”
When eviction attempts to remove the page:
- scan hazard pointers
- if referenced → skip eviction
This allows lock-free reads of in-memory pages.
Lock-Free Writes: Skip Lists
Updates are stored in an in-memory skip list.
This structure enables multiple threads to insert updates concurrently.
Insertion uses atomic Compare-And-Swap (CAS).
Example pattern:
while true:
old = pointer
new = create_update()
if CAS(pointer, old, new):
break
If another thread wins the race, the operation simply retries.
This eliminates the need for write locks.
Putting It All Together
The architecture combines three independent mechanisms.
flowchart TD
Client --> Transaction
Transaction --> Snapshot
Snapshot --> ReadVersion
ReadVersion --> UpdateChain
ReadVersion --> DiskPage
UpdateChain --> SkipList
DiskPage --> Cache
Cache --> HazardPointers
Cache --> Reconciliation
Each layer solves a different problem:
| Layer | Problem Solved |
|---|---|
| MVCC | transactional consistency |
| Snapshot isolation | stable reads |
| Skip list | concurrent updates |
| Hazard pointers | safe memory access |
Disk vs Memory Concurrency Models
Now we can compare the two concurrency strategies used by WiredTiger.
Disk-Level Concurrency (MVCC)
Used for transactional correctness.
Mechanisms:
- MVCC
- version chains
- timestamps
- snapshot isolation
- reconciliation
Characteristics:
- multiple versions of records
- consistent transaction snapshots
- readers never block writers
Goal:
correct transactional semantics.
Memory-Level Concurrency (Lock-Free Structures)
Used for CPU scalability.
Mechanisms:
- hazard pointers
- lock-free skip lists
- atomic CAS operations
Characteristics:
- no read locks
- no page write locks
- safe memory access
- extremely high concurrency
Goal:
maximize throughput on multi-core CPUs.
Key Differences
| Feature | Disk Layer | Memory Layer |
|---|---|---|
| Purpose | transactions | concurrency |
| Technique | MVCC | lock-free algorithms |
| Data structure | B+Tree pages | skip lists |
| Isolation | snapshot isolation | not transactional |
| Memory safety | N/A | hazard pointers |
| Update model | version chains | CAS pointer updates |
Why This Hybrid Design Works
Separating the system into two layers allows WiredTiger to optimize each independently.
Disk layer focuses on:
- durability
- transactional correctness
- efficient storage
Memory layer focuses on:
- CPU scalability
- lock-free algorithms
- low-latency pointer operations
The result is a system that achieves both:
- strong transactional guarantees
- extremely high concurrency
Key Takeaways
Modern storage engines separate transactional logic from concurrency control.
WiredTiger achieves this using a layered design:
- MVCC provides transactional consistency
- snapshot isolation guarantees stable reads
- update chains maintain multiple versions
- skip lists enable lock-free writes
- hazard pointers protect in-memory pages
This architecture allows MongoDB to support:
- high transaction throughput
- massive parallelism
- minimal lock contention
Final Thoughts
The most interesting part of WiredTiger’s design is how it combines classic database ideas with modern lock-free techniques.
Traditional databases focused heavily on:
- locks
- latches
- serialized writes
WiredTiger instead embraces:
- versioned data
- optimistic concurrency
- atomic pointer updates
The result is a storage engine that scales across modern multi-core systems while still providing the transactional guarantees developers expect.
Understanding this layered architecture provides a deeper appreciation of how modern databases achieve both consistency and extreme concurrency.