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.
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.
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.
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.
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.
2 · Table lock
Per-table read/write. The reader-writer lock you drove above.
3 · B+ Tree lock
Guards structural changes — splits and re-linking.
4 · StorageEngine lock
Page-level disk I/O.
5 · PageCache / ExtentCache locks
The LRU list mutations from Module 2.
The contract, written in code
/// 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.
Read lock vs. write lock
The same try/finally shape, one difference: who's allowed in alongside you.
// 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.
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.
Spot the ordering violation
Using the lock stack above, find the line that grabs locks in the wrong order.
public void Weird() { _cache.Lock.EnterWriteLock(); // level 5 try { _storage.Lock.EnterWriteLock(); // level 4 — after 5! // ... do work ... } finally { _cache.Lock.ExitWriteLock(); } }
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.
Check yourself
Method1 and Method2 at the same time. First thing to check?BPlusTree use a plain lock(obj) instead of a reader-writer lock?SelectAll(). What does it see?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.