A warm welcome back to the third edition of The Main Thread, straight from the buzzing tech hubs of Bangalore. After wrapping up the deep-dive on tuning parameters of B-Trees in real-world systems, I have been reflecting on Robert Greene’s “The 48 Laws of Power“ - it turns out mastering concurrency in databases is a lot like wielding influence in a chaotic systems.

Sometimes, it’s good to draw parallels from other fields to understand how systems in an unrelated field work.

In the last issue, we talked about packing nodes tight and collapsing height to reduce disk I/Os. That’s all fine in a single-threaded world. But in reality - databases are battlegrounds: thousands of queries slamming in simultaneously, reads mixing with writes, all while keeping the data consistent and being performant. That’s where concurrency engineering kicks in, juggling theory with production-grade resilience.

Let’s get into how real-world DBs handle the frenzy without breaking.

Surviving the storm of concurrent updates

Databases don’t just store data into B-Trees and call it a day. They have to update these structures concurrently, often with hundreds or thousands of concurrent requests coming in.

One query is scanning the index, while another is inserting a new key, yet another is deleting. Without smart safeguards, we would end up with corrupted pointers, lost data, or deadlocks.

Enter locks and latches - the unsung heroes that make massive scale concurrency possible. But they are not interchangeable - mix them up, and we are in for pain.

Locks vs. Latches: The Logical vs. The mechanical

First of all, let’s clear the fog on the difference, because I have seen too many engineers (myself included, early on) treat them as synonyms. They are *not*.

Locks

  • Logical locks are about transactional correctness - the “ACID“ promise that keeps your data sane.

  • They are high-level constructs that protect range of keys or entire records from conflicting operations. Think of them as semantic guards - “Hey, I am updating this user’s balance, no one else touches it until I am done.”

  • They ensure isolation in transactions, preventing dirty reads or lost updates.

  • In a B-Tree context, a logical lock might cover a key range during an insert, making sure no one else modifies that slice during splitting or merging.

Latches

  • Latches are low-level, thread-safety primitives → basically lightweight mutexes for in-memory access.

  • They are not about transactions; they are about preventing raw memory corruption when multiple threads poke at the same page simultaneously.

  • Latches are short-lived (microseconds), grabbed and released during traversal or modification of a node.

  • Without them, two threads could both try to update a node’s key array at once, causing garbage data or crashes.

Pragmatic tip from the trenches:

Locks can be held across I/Os (e.g., while reading a page from the disk) but latches never should. They are strictly in-memory to avoid blocking the world.

In Postgres, latches protect buffer pool pages while locks handle row-level or index-range consistency.

Get this balance wrong in your own storage engine, and you will see throughput tank under load.

Readers-Writers Locks

Now, not all access is equal. Most workloads are read-heavy. So, databases use readers-writers lock to optimize: multiple readers can share access to a resource, but writers need exclusive control.

In B-Trees, this plays out beautifully. A reader (let’s say a SELECT query) grabs a shared latch on a node, zips through it binary search to find the child pointer and moves on, without blocking other readers.

On the other hand, writers need an exclusive latch to insert, delete, or split, ensuring no one’s reading mid-modification.

But there’s real engineering art: optimistic strategies. Many systems, like InnoDB in MySQL lets readers proceed without full-locks, then verify consistency post-read. If a write snuck in, retry. It’s a trade-off: fewer blocks upfront but do-overs under contention.

The key is monitoring: track latch contention metrics (e.g. wait time in pg_stat_bgwriter) and adjust buffer pool sizes accordingly.

Real World Example: Two Writers Colliding on an Insert

Picture this: Two transactions inserting keys into the same B-Tree index at the same time - say, two users signing up with similar email prefixes, landing in the same leaf node.

Thread A starts descending: latches the root (shared, since it is reading initially), binary searches to the child, unlatches, latches the next level. It reaches the leaf, seeing it’s full, upgrades to exclusive latch, and starts splitting: creates a new sibling node, moves half the keys over, updates pointer.

Meanwhile, Thread B is hot on its heels, following the same path. Without proper coupling, B might see an inconsistent state - maybe latching the old leaf after A’s split but before the parent update. Boom: lost insert or data corruption happened.

The fix to this concurrency problem is “latch coupling“. B grabs the next latch before releasing the current one, ensuring a safe hand-off. If A’s split propagates up, B detects the change and retries from the parent. Logical locks layer on top: both might hold shared range locks during descent, escalating to exclusive on the target key to prevent phantom inserts.

In a system I once scaled from 1.5k to 7k TPS, this was the difference between smooth sailing and constant outages. Sequential inserts (e.g., auto-increment IDs) hotspot the rightmost leaf, amplifying contention - mitigate with reverse indexes or partitioning.

Takeaway: Where Theory Hits the Pavement

Concurrency in B-Trees is that gritty intersection of algorithms and reality: you can have the most efficient structure on paper, but without dialed-in locks and latches, it crumbles under load. It’s about ensuring correctness without sacrificing performance - balancing shared access for reads, exclusive for writes, and always monitoring for bottlenecks.

The next time you are building or tuning databases, start here next time load spikes: profile latch waits, audit lock escalations, and remember, the buffer pool is your amplifier.

Loving these deep dives? Drop your concurrency war stories in the replies. And if you have not grabbed previous one on B-Tree bandwidth tricks, hit the archive.

Until next time, keep engineering smart. Namaste.

— Anirudh

P.S. For hands-on folks, check my GitHub repo Database Mechanics for the B-Tree implementation. Experiment away.

Keep reading

No posts found