Database Internalsmodule 6 of 8
Module 06 ~15 min · locks & deadlock

Concurrency
Many hands, one ledger

Two users click "Buy" on the last ticket at the exact same millisecond. Who gets it? If the database fumbles this question even once, the business loses trust. Concurrency control is the invisible layer that answers it correctly, billions of times a day.

🚇 Hold this picture

A revolving door at a subway station. Many people can walk through at once in the same direction (many readers) — but if one person stops to tie their shoelace in the middle (a writer), everyone else must wait. A bad door either lets people crash into each other (corrupted data) or blocks everyone for every single person (deadlock and molasses). The lock scheme is the door's design.

◆ The key insight

Reader-writer locks let you have your cake (many parallel reads) and eat it (exclusive writes), as long as writes are rarer than reads. And strict lock ordering prevents the deadly embrace called deadlock — where thread A waits on B while B waits on A.

Why should you care?

Concurrency bugs are the hardest to reproduce and the most dangerous in production. When AI sprinkles lock(something) at random, you need to recognize the smell. Knowing your system's lock order is like knowing the emergency exits — rarely needed, priceless when it is.

01

First, the danger: a race condition

Why do we need locks at all? Because without them, two threads touching the same data at the same instant can corrupt it. Below, two threads each deposit $50 into one account. Run it without a lock, then with one, and check the final balance against the obvious answer — $200.

Two deposits, one accountread · modify · write
shared balance
$100
THREAD A · deposit $50
idle
local copy:
THREAD B · deposit $50
idle
local copy:
02

The fix for reads vs. writes: drive the lock

A reader-writer lock has three states. Add readers (many can share) and try to send a writer in (it needs everyone out first). Watch the gate accept or block each arrival.

ReaderWriterLockSlimmany readers · or one writer · never both
IDLE
nobody holds it
READERS
0
shared access
WRITER
exclusive (1)
03

The five-level lock stack

When code needs more than one lock, it must always take them in this order, top to bottom. That single rule is what makes deadlock impossible.

🔐

1 · Database lock

Schema-level operations — like creating a table.

level 1
🔒

2 · Table lock

Per-table read/write. The reader-writer lock you drove above.

level 2
🔑

3 · B+ Tree lock

Guards structural changes — splits and re-linking.

level 3
🗝️

4 · StorageEngine lock

Page-level disk I/O.

level 4
📎

5 · PageCache / ExtentCache locks

The LRU list mutations from Module 2.

level 5
04

The contract, written in code

Storage/StorageEngine.cs
/// Lock Ordering (to prevent deadlocks):
/// When multiple locks must be acquired, always take them
/// in this order:
///   1. Database._lock      (schema operations)
///   2. Table._lock         (table operations)
///   3. BPlusTree._lockObject (index operations)
///   4. StorageEngine._lock (storage operations)
///   5. PageCache / ExtentCache locks
public class StorageEngine : IDisposable
{
    private readonly ReaderWriterLockSlim _lock;
}
A rule, not a guardrail

This is a comment — a contract. The compiler will not enforce it.

But if every method that takes two locks always grabs them in this order, two threads can never form a cycle. Thread A holding level 2 and wanting level 3 can't be waiting on thread B that holds level 3 and wants level 2 — because B would have taken level 2 first.

Consistent ordering is the entire defense against deadlock.

05

Read lock vs. write lock

The same try/finally shape, one difference: who's allowed in alongside you.

Table.cs — SelectByKey vs Insert
// READ — many threads at once
public DataRow? SelectByKey(object key)
{
    _lock.EnterReadLock();
    try { return Read(key); }
    finally { _lock.ExitReadLock(); }
}

// WRITE — exclusive, blocks all others
public void Insert(DataRow row)
{
    _lock.EnterWriteLock();
    try { WriteIndex(row); }
    finally { _lock.ExitWriteLock(); }
}
The crucial difference

EnterReadLock — many readers hold it simultaneously. Reads don't conflict, so they run in parallel.

EnterWriteLock — exclusive. One writer, and every reader and writer waits.

Both use try / finally. The lock is released no matter what — even if an exception is thrown inside. A lock leaked by a missing finally hangs the whole table forever.

06

The new danger: the Deadlock Lab

Locks cure the race condition — but they introduce a fresh hazard. Drive two threads and two locks yourself. In risky mode the threads grab locks in opposite orders; step them alternately (A, B, A, B) and watch them freeze in a deadlock. Then switch to safe mode — one consistent order — and try as hard as you can to break it. You won't be able to.

Deadlock Labtwo threads · two locks · who waits for whom
lock order
💬 #the-deadly-embrace — the same story, narrated
07

Spot the ordering violation

Using the lock stack above, find the line that grabs locks in the wrong order.

Click the line that breaks lock ordering
public void Weird()
{
    _cache.Lock.EnterWriteLock();        // level 5
    try
    {
        _storage.Lock.EnterWriteLock();  // level 4 — after 5!
        // ... do work ...
    }
    finally { _cache.Lock.ExitWriteLock(); }
}
hint: compare each lock's level to the one above it…
Found it. This grabs the cache lock (level 5) and then the storage lock (level 4) — backwards. A sibling thread following the rules could be holding storage (4) and waiting for cache (5). Now each holds what the other needs: deadlock. The fix is to always take level 4 before level 5.

Lock ordering is a design discipline, not a runtime check. The compiler won't stop you from violating it — only review will. Every new method that takes two locks is a potential deadlock until proven otherwise.

Coarse vs. fine-grained. This engine locks whole tables at once — simple, but one writer blocks every reader of that table. Real databases use row- or page-level locks for more parallelism, at the cost of far more complex code. Simplicity is a legitimate design choice.

08

Check yourself

Scenario
Reads are fast, but writes sometimes stall for seconds. What's likely happening?
Correct. A write lock is exclusive. While a slow writer holds it, no reader or writer can proceed on that table. That's the cost of coarse, table-level locking.
Debugging
The app hangs forever when two threads call Method1 and Method2 at the same time. First thing to check?
Right. A permanent hang under concurrency screams deadlock. Compare the lock acquisition order of the two methods — mismatched order is the classic cause.
Architecture
Why does BPlusTree use a plain lock(obj) instead of a reader-writer lock?
Exactly. A split mutates pointers across nodes. A reader mid-split could follow a half-updated link. Exclusive locking sidesteps the whole class of bug.
Scenario
You're editing a row inside a transaction. Another thread calls SelectAll(). What does it see?
Correct. Deferred writes (Module 5) mean in-flight changes live in a buffer, not the index. Other threads see the committed state — that's isolation in action.

Up next, the finale: the engine is fast, durable, and safe under load. But how do you see it working while it runs? Metrics, structured logs, and a live dashboard.