What Is Database Locking?
The complete guide to how databases let hundreds of people touch the same data at once without breaking it — explained from first principles, with real production examples from Amazon, Uber, and Netflix.
Introduction & History
Imagine a single notebook that an entire family shares to write down chores. If two people try to write in it at the exact same instant, pages get torn, ink smears, and nobody can read what was written. A database is that notebook — except instead of one family, it might be shared by millions of people (or millions of computer programs) all trying to read and write at the same time.
Database locking is the mechanism a database uses to make sure that when two or more users try to touch the same piece of data at the same time, they don’t step on each other’s toes. It is the traffic‑light system of the data world: it tells one car “go” and the other car “wait,” so nobody crashes at the intersection.
1.1 A Short History
In the early days of computing (1960s–1970s), most programs ran one at a time, one after another (this is called “batch processing”). There was no real fight over data because only one program touched the database at any given moment.
As computers became powerful enough to run many programs simultaneously (multiprogramming) and later as networks connected many users to one shared database, engineers ran into a brand‑new problem: what happens when two people try to update the same bank account balance at the same second? This question gave birth to an entire field of computer science called concurrency control, and locking was one of the very first (and still most widely used) solutions.
Jim Gray, a legendary computer scientist at IBM, formalised much of this theory in the 1970s while working on System R, one of the first relational database prototypes. His work on locking, transactions, and the concept of ACID properties (which we’ll explain shortly) still forms the backbone of every serious database today — MySQL, PostgreSQL, Oracle, SQL Server, and even modern cloud databases like Amazon Aurora and Google Spanner.
Think of a public bathroom with a single key hanging outside. Whoever holds the key can go in and lock the door. Everyone else has to wait outside until the key is returned. That key is a “lock,” and the waiting people are “blocked.”
1.2 A Quick Timeline
- 1960s–70sEarly mainframe computers run programs in strict batch mode; concurrency conflicts are rare and mostly ignored.
- 1970sJim Gray and colleagues at IBM formalise concurrency control theory as part of the System R relational database project.
- 1980s–90sCommercial relational databases (Oracle, DB2, SQL Server, Sybase) ship production‑grade locking and isolation‑level support.
- 1990s–2000sMVCC gains popularity in PostgreSQL, Oracle, and later MySQL InnoDB — readers stop blocking writers for good.
- 2010sDistributed lock services (ZooKeeper, etcd, Redis Redlock) let systems coordinate across many machines and data centres.
- 2012‑presentGlobal databases like Google Spanner and CockroachDB combine locking with consensus algorithms to serialise transactions across continents.
The Problem: Why Locking Exists
To understand why locking exists, we first need to understand what goes wrong without it. Let’s use the simplest possible example: a bank account.
Suppose your bank balance is $100. You and your sibling both try to withdraw $80 at the exact same second — you at an ATM, your sibling using the banking app. Without any protection, here is what could happen:
- Step 1Both programs read the balance. Your ATM reads balance = $100. At the same instant, the app also reads balance = $100.
- Step 2Both programs calculate a new balance. ATM computes: 100 − 80 = 20. App computes: 100 − 80 = 20.
- Step 3Both programs write the new balance. Both write “20” back to the database. Two withdrawals of $80 happened, but the balance only dropped by $80 instead of $160.
- Step 4The bank loses $80. This is called a lost update — one of the classic concurrency bugs. The bank gave out $160 but only recorded an $80 decrease.
This is exactly the kind of catastrophe that locking prevents. A lock makes sure that once the ATM starts working with your balance, the app must wait until the ATM is completely finished before it’s allowed to read or change that same balance.
Fig 2.1 — Locking forces the second transaction to wait until the first one finishes, preventing the lost‑update bug.
2.1 The Other Classic Problems Locking Prevents
| Problem | What Happens | Real‑World Feel |
|---|---|---|
| Dirty Read | One transaction reads data that another transaction hasn’t finished writing (and might undo) | Reading a draft email before the sender hits “send” — and then it gets deleted |
| Non‑Repeatable Read | You read the same row twice in one transaction and get two different answers | Checking a product’s price twice during checkout and it changed in between |
| Phantom Read | A query run twice returns a different set of rows because new rows were inserted | Counting people in a room, then counting again and finding 2 extra people walked in |
| Lost Update | Two writes overwrite each other, and one update disappears completely | The bank withdrawal example above |
These aren’t academic curiosities. Lost updates and dirty reads have caused real financial losses, double‑booked airline seats, and oversold e‑commerce inventory in production systems. Locking (or its modern alternatives) is the primary defence.
Core Concepts
Before going further, let’s build a solid vocabulary. Every term below is something you will see constantly in database documentation, interviews, and production incident reports.
3.1 What Is a “Lock”?
A lock is a flag (a small marker) that the database attaches to a piece of data — a row, a page, or an entire table — to say “this is currently being used, please wait or be careful.” It’s stored in the database engine’s memory, in a structure usually called a lock table.
WhatA permission slip that a transaction must hold before touching certain data.
WhyTo prevent two transactions from making conflicting changes at the same time.
WhereInside every relational database (MySQL, PostgreSQL, Oracle, SQL Server) and even in in‑memory data structures inside your own Java or Python programs.
AnalogyA “Do Not Disturb” sign on a hotel room door.
3.2 Shared Locks vs. Exclusive Locks
Not every lock is the same strictness. There are two fundamental types:
Shared Lock (S‑Lock)
Used for reading. Many transactions can hold a shared lock on the same data at once — because reading doesn’t change anything, it’s safe to let many people read together. Like several people reading the same newspaper at once.
Exclusive Lock (X‑Lock)
Used for writing. Only one transaction may hold an exclusive lock, and while it’s held, no one else can read OR write that data. Like being the only person allowed to edit a shared Google Doc while it’s “locked for editing.”
This gives us a simple compatibility rule that every database engine follows:
| Held Lock | Requested Lock | Result |
|---|---|---|
| Shared (S) | Shared (S) | Allowed — both can read |
| Shared (S) | Exclusive (X) | Must wait |
| Exclusive (X) | Shared (S) | Must wait |
| Exclusive (X) | Exclusive (X) | Must wait |
3.3 Lock Granularity
Granularity just means “how big is the chunk of data being locked?” Databases can lock at different sizes:
Row‑level
Locks a single row. Very precise — only that one record is affected. Best for high‑concurrency apps (used by MySQL InnoDB, PostgreSQL by default).
Page‑level
Locks a “page” (a block of data, often 8 KB) that contains several rows. A middle ground used internally by some engines.
Table‑level
Locks the entire table. Simple and cheap to manage, but blocks everyone else — like closing an entire library because one person is reading one book.
Finer granularity (row‑level) = more concurrency but more overhead to track. Coarser granularity (table‑level) = less overhead but more waiting. Modern OLTP databases default to row‑level locking because most apps need high concurrency.
3.4 A Tiny Beginner Example
Imagine a spreadsheet shared with your classmates. If your teacher locks cell B2 while grading it, nobody else can edit B2 until the teacher is done — but everyone can still edit A1, C5, or any other cell. That’s exactly how row‑level locking behaves inside a database table.
3.5 Software Example — SELECT…FOR UPDATE
In SQL, you often request an exclusive lock explicitly using SELECT … FOR UPDATE:
-- Transaction A
BEGIN;
SELECT balance FROM accounts WHERE id = 123 FOR UPDATE; -- takes an exclusive row lock
UPDATE accounts SET balance = balance - 80 WHERE id = 123;
COMMIT; -- lock released hereWhile Transaction A holds this lock, any other transaction trying to SELECT … FOR UPDATE the same row (id = 123) will simply pause and wait until Transaction A commits or rolls back.
3.6 Latches vs. Locks — A Subtle but Important Distinction
Interviewers love this question, so let’s settle it clearly. A lock protects logical data (a row, a table) for the entire duration of a transaction, and is visible to the transaction manager. A latch (sometimes called a “mutex” inside the database engine) protects a physical, low‑level structure — like a page in memory or an internal data structure — for a tiny fraction of a second, just long enough to safely read or modify it. Latches are held for microseconds; locks can be held for the whole length of a transaction, which might be seconds or even minutes.
Think of a lock as reserving a whole meeting room for your one‑hour meeting, while a latch is like briefly holding a door open for half a second so nobody bumps into you while you walk through. Both prevent conflicts, but they operate at completely different time scales and for completely different purposes.
Isolation Levels & the SQL Standard
Locking doesn’t exist in a vacuum — it’s the tool databases use to implement something called transaction isolation levels. These levels are a dial you can turn between “fast but risky” and “slow but safe.”
A transaction is simply a group of operations treated as one unit — either all of them succeed, or none of them do. Think of it like a recipe: you don’t want to add “half an egg”; you either finish the whole step or you don’t do it at all.
| Isolation Level | Dirty Read | Non‑Repeatable Read | Phantom Read | Typical Locking Behaviour |
|---|---|---|---|---|
| Read Uncommitted | Possible | Possible | Possible | Almost no locks — reads see everything, even uncommitted changes |
| Read Committed | Prevented | Possible | Possible | Short‑lived read locks, released immediately after each statement |
| Repeatable Read | Prevented | Prevented | Possible* | Read locks held until transaction ends |
| Serializable | Prevented | Prevented | Prevented | Range locks / strictest locking, transactions behave as if run one‑at‑a‑time |
*Note: PostgreSQL’s Repeatable Read actually prevents phantom reads too, thanks to MVCC snapshots — more on this in Section 8.
Isolation levels are like different security settings on a shared document. “Read Uncommitted” is like letting people see your live typing, even typos you haven’t saved. “Serializable” is like forcing everyone to take turns, one editor at a time, so it’s as if there was never any overlap at all.
MySQL’s default is Repeatable Read. PostgreSQL’s default is Read Committed. Oracle only offers Read Committed and Serializable. Knowing your database’s default is a very common interview and production‑debugging topic.
4.1 Setting Isolation Level in Java (JDBC)
Your application code can request a specific isolation level per connection, overriding the database default when a particular operation needs stronger (or can tolerate weaker) guarantees:
Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);
conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
try {
// critical financial operation that must never see anomalies
transferFunds(conn, fromId, toId, amount);
conn.commit();
} catch (SQLException e) {
conn.rollback();
throw e;
} finally {
conn.setAutoCommit(true);
}A simple beginner way to remember the four levels: Read Uncommitted is “trust everyone, even mid‑sentence.” Read Committed is “only listen to finished sentences.” Repeatable Read is “once I start listening to your story, don’t change earlier parts on me.” Serializable is “we can only talk one at a time, in turns, no interruptions at all.”
Two‑Phase Locking (2PL)
Two‑Phase Locking is the classical algorithm that guarantees the strictest, safest form of concurrency (serialisability). It has a wonderfully simple rule:
“Once a transaction releases even one lock, it may never acquire any new locks again.”
This creates exactly two phases:
Growing Phase
The transaction is free to acquire as many locks as it needs. No locks are released yet.
Shrinking Phase
The transaction starts releasing locks. Once this phase begins, it can never acquire a new lock again.
Fig 5.1 — The “growing” phase acquires locks, the “shrinking” phase releases them — they never interleave.
Most production databases use a variant called Strict Two‑Phase Locking (Strict 2PL), where all exclusive locks are held until the transaction fully commits or rolls back (rather than releasing gradually). This avoids a subtle problem called “cascading rollbacks,” where undoing one transaction would force you to undo others that read its uncommitted changes.
Optimistic vs. Pessimistic Locking
There are two philosophies for handling conflicts, and picking the right one is one of the most important production decisions a backend engineer makes.
Pessimistic Locking
- Assumes conflicts are likely, so it locks data before touching it.
- Other transactions must wait.
- Great for high‑contention data (e.g. a popular concert ticket).
- Can reduce throughput because of waiting.
Optimistic Locking
- Assumes conflicts are rare, so it does not lock upfront.
- Reads data, does work, then checks “did anyone change this while I worked?” before saving.
- If a conflict is found, the write is rejected and retried.
- Great for low‑contention data and high‑read workloads.
Optimistic locking is usually implemented with a version number or a timestamp column.
// Java example using JDBC – optimistic locking with a version column
public boolean updateProductPrice(Connection conn, int productId, double newPrice, int expectedVersion) throws SQLException {
String sql = "UPDATE products SET price = ?, version = version + 1 " +
"WHERE id = ? AND version = ?";
try (PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setDouble(1, newPrice);
stmt.setInt(2, productId);
stmt.setInt(3, expectedVersion);
int rowsAffected = stmt.executeUpdate();
// If rowsAffected == 0, someone else changed the row first — retry or fail
return rowsAffected > 0;
}
}If another transaction already bumped the version number, rowsAffected will be 0, and your application knows a conflict happened — it can retry the whole operation with fresh data.
Pessimistic locking is like reserving a library book before you even start reading — nobody else can touch it. Optimistic locking is like grabbing a book off the shelf, assuming no one else wants it right now, but checking at checkout whether someone else already took it out from under you.
This pattern is exactly how popular frameworks like Java’s JPA/Hibernate implement @Version fields, and it’s how most e‑commerce “add to cart” and inventory systems avoid overselling without locking every single row constantly.
Deadlocks: Detection & Prevention
A deadlock happens when two (or more) transactions are each waiting for a lock that the other one holds — forever. Neither can proceed. It’s the database equivalent of two polite people in a doorway, each saying “after you,” and both never moving.
Fig 7.1 — A classic deadlock cycle — Transaction 1 waits on Transaction 2, which waits on Transaction 1.
7.1 Beginner Example
-- Transaction 1
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 1; -- locks row 1
-- (pauses here for a moment)
UPDATE accounts SET balance = balance + 50 WHERE id = 2; -- wants to lock row 2
-- Transaction 2 (running at the same time)
BEGIN;
UPDATE accounts SET balance = balance - 20 WHERE id = 2; -- locks row 2
-- (pauses here for a moment)
UPDATE accounts SET balance = balance + 20 WHERE id = 1; -- wants to lock row 1Transaction 1 holds row 1 and wants row 2. Transaction 2 holds row 2 and wants row 1. Neither will ever get what it needs. This is a genuine deadlock.
7.2 How Databases Detect Deadlocks
Most databases build a wait‑for graph in the background — a diagram of “who is waiting for whom.” If this graph ever forms a cycle (as in the figure above), the database knows a deadlock exists. It then picks a victim transaction (often the one that has done the least work) and forcibly rolls it back, freeing its locks so the other transaction can continue.
- Both transactions request locks and one becomes blocked.
- The database’s deadlock detector periodically scans the wait‑for graph.
- A cycle is found → deadlock confirmed.
- The database aborts one transaction (throws an error like Deadlock found; try restarting transaction).
- The application catches this error and retries.
7.3 How to Prevent Deadlocks in Practice
Consistent ordering
Always lock resources in the same order across your whole application (e.g. always update the lower account ID first).
Keep transactions short
The less time a transaction holds locks, the smaller the window for a deadlock to occur.
Use lower isolation when safe
Read Committed acquires fewer, shorter‑lived locks than Serializable.
Add retry logic
Deadlocks are sometimes unavoidable at scale — code your app to gracefully retry.
// Java example — handling a MySQL deadlock with a retry loop
int maxRetries = 3;
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
transferFunds(conn, fromAccountId, toAccountId, amount);
break; // success
} catch (SQLException e) {
if (e.getErrorCode() == 1213 /* MySQL deadlock code */ && attempt < maxRetries) {
Thread.sleep(50L * attempt); // small backoff before retrying
continue;
}
throw e;
}
}A Quick Detour: Locking Inside Your Application Code
Before we go further into the database engine itself, it helps to see that the exact same “wait your turn” idea exists inside ordinary application code — not just inside the database. Java, for example, gives you two common tools for this:
// Option 1: the "synchronized" keyword — simple, built into the language
public class Counter {
private int count = 0;
public synchronized void increment() {
count++; // only one thread can run this method at a time
}
}
// Option 2: java.util.concurrent.locks.ReentrantLock — more flexible
public class SafeCounter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // ALWAYS release in a finally block
}
}
}These application‑level locks protect data living inside a single running program’s memory (like a shared counter used by multiple threads). Database locks solve the exact same core problem — “only one at a time” — but for data that lives on disk and is shared across many separate programs, servers, and even data centres. The underlying idea (mutual exclusion) is identical; only the scale and the tooling differ.
A database connection pool combined with unsynchronised in‑memory caching in your Java service can reintroduce the exact same race conditions database locks were meant to prevent — for example, caching a “current balance” in application memory without any synchronisation. Always ask: is this data shared across threads, across processes, or both? Each layer needs its own protection.
MVCC — The Modern Alternative to Locking Reads
Locking every single read would be extremely slow at scale, so most modern databases (PostgreSQL, MySQL InnoDB, Oracle, SQL Server’s snapshot isolation) use something smarter for reads: Multi‑Version Concurrency Control (MVCC).
The idea is beautifully simple: instead of making readers wait for writers, the database keeps multiple versions of a row. A reader is given a consistent “snapshot” of the data as it looked at the moment their transaction started, while a writer creates a brand‑new version of the row rather than overwriting the old one in place.
Fig 8.1 — MVCC lets readers keep working with an old but consistent snapshot while writers create new versions.
Think of Google Docs’ version history. You can keep reading an older saved version of a document while someone else is actively editing and saving a newer version — you’re never forced to stop reading just because they’re writing.
With MVCC, readers never block writers, and writers never block readers. Only writer‑vs‑writer conflicts still need locks. This is why PostgreSQL and MySQL InnoDB can support thousands of concurrent read‑heavy connections without grinding to a halt.
Old row versions that are no longer needed by any active transaction are eventually cleaned up by a background process — in PostgreSQL this is called VACUUM, and forgetting to run it (or having it fall behind) is a famous cause of production slowdowns and “table bloat.”
Architecture & Internal Components
Let’s open the hood and look at the actual pieces inside a database engine that make locking possible.
Fig 9.1 — A simplified architecture of how the lock manager fits into a database engine.
9.1 Key Components
Transaction Manager
Coordinates the lifecycle of every transaction: begin, commit, rollback. Talks to the lock manager whenever data is touched.
Lock Manager
The brain of locking. Grants, queues, and releases locks. Decides who waits and who proceeds.
Lock Table
An in‑memory data structure (often a hash map keyed by resource ID) tracking which locks are held, by whom, and who’s waiting.
Deadlock Detector
Runs periodically (or on every wait) to find cycles in the wait‑for graph and pick a victim.
Write‑Ahead Log (WAL)
Records every change before it’s applied, so the database can recover after a crash without losing committed data.
Buffer Pool
An in‑memory cache of disk pages so reads/writes don’t always hit slow disk directly.
The lock table deserves extra attention because it’s frequently asked about in interviews. Internally it’s typically a hash table where the key is something like (table_id, row_id) and the value is a small structure listing: the lock mode (S or X), the list of transactions currently holding it, and a queue of transactions waiting for it.
9.2 How the Lock Manager Decides “Grant” or “Wait”
When a new lock request arrives, the lock manager runs roughly this algorithm:
- Look up the resource in the lock table. If no entry exists, create one and grant the lock immediately.
- If an entry exists, compare the requested mode against every currently granted mode using the compatibility matrix from Section 3.2.
- If compatible with all current holders and the wait queue is empty, grant immediately.
- If incompatible, or if there are already waiters ahead in line (to keep things fair and prevent starvation), append the request to the wait queue and put the requesting transaction to sleep.
- Whenever a lock is released, wake up the next compatible request(s) in the queue in order.
That “fairness” rule in step 4 is important: without it, a steady stream of incoming shared (read) locks could keep an exclusive (write) lock waiting forever — a problem called writer starvation. Most production databases use FIFO queuing specifically to prevent this.
Data Flow & Lifecycle of a Lock
Let’s trace exactly what happens, step by step, from the moment a transaction starts to the moment it ends.
Fig 10.1 — The full lifecycle of a lock request, from acquisition to release.
- Begin: the application starts a transaction.
- Request: when the transaction touches a row, the engine asks the lock manager for the appropriate lock (S for read, X for write).
- Check compatibility: the lock manager checks the lock table to see if the requested lock conflicts with existing locks.
- Grant or Queue: if compatible, the lock is granted immediately. If not, the requester is placed in a FIFO wait queue (most databases are fair and avoid starving waiters).
- Hold: the transaction keeps working while holding its locks.
- Release: on COMMIT or ROLLBACK, all locks are released at once (in Strict 2PL).
- Wake next waiter: the lock manager notifies the next transaction in the queue that the resource is now free.
Advantages, Disadvantages & Trade-offs
Locking is a powerful tool, but every tool has costs. Understanding the trade‑offs is what turns knowledge into good production judgement.
Advantages of Locking
- Guarantees data correctness under concurrent access.
- Well‑understood, decades of proven use in production.
- Prevents lost updates, dirty reads, and other anomalies.
- Works predictably even for write‑heavy workloads.
Disadvantages of Locking
- Reduces concurrency — waiting transactions slow down throughput.
- Can cause deadlocks that must be detected and resolved.
- Adds memory and CPU overhead to track lock state.
- Coarse‑grained locks (table‑level) can create bottlenecks.
| Strategy | Best For |
|---|---|
| Row‑level locking | The default for OLTP apps with many small writes. |
| MVCC | Read‑heavy workloads that must avoid blocking readers. |
| Optimistic locking | Low‑contention writes where conflicts are rare. |
| Table‑level locking | Simplest to reason about, but has the lowest concurrency. |
There’s no universally “correct” locking strategy — it’s always a trade‑off between consistency, concurrency, and complexity. This trio is sometimes nicknamed the “consistency‑concurrency dial”: turning it toward more safety turns it away from more speed.
Performance & Scalability
Locking directly affects how many transactions per second (TPS) your database can handle. Here are the levers engineers pull in production.
12.1 Lock Contention
Contention happens when many transactions want the same lock at once — like everyone in a store trying to use the single self‑checkout machine. High contention on a “hot row” (e.g. a popular product’s inventory count) is one of the most common real‑world scalability bottlenecks.
12.2 Techniques to Reduce Contention
Sharding hot rows
Split one counter into multiple partial counters (e.g. 10 separate “stock” rows summed together) so writes spread across more locks.
Batching
Group multiple small updates into fewer, larger transactions to reduce the number of lock acquisitions.
Shorter transactions
Do expensive computation (like calling an external API) BEFORE opening a transaction, not while holding locks.
Right isolation level
Don’t default to Serializable everywhere — use the loosest level that still keeps your data correct.
12.3 Lock Escalation
Some databases (notably SQL Server) automatically convert many small row‑locks into one big table‑lock when a transaction touches too many rows, to save memory. This is called lock escalation, and while it saves overhead, it can accidentally block unrelated transactions — a subtle production gotcha worth knowing.
A very common real‑world bug: an engineer runs a huge UPDATE touching millions of rows during business hours. Row locks escalate to a table lock, and the entire application freezes. The fix: batch large updates into small chunks (e.g. 1,000 rows at a time) with brief pauses.
12.4 Index Locking — The Hidden Cost
Beginners often forget that an UPDATE doesn’t just lock the row itself — it may also need to lock entries in every index built on that table, since indexes are separate physical structures that must stay in sync with the underlying data. A table with five indexes touched by a write may involve five separate internal lock acquisitions, not just one. This is one reason why adding “just one more index” to speed up reads can quietly slow down writes — a classic read‑versus‑write trade‑off every engineer eventually learns the hard way.
12.5 Lock‑Free and Latch‑Free Data Structures
At the very cutting edge of performance engineering, some in‑memory databases and caching layers (like parts of Redis, or specialised in‑memory databases such as VoltDB) use lock‑free algorithms built on hardware‑level atomic operations (like “compare‑and‑swap”). These structures avoid traditional locks entirely for certain operations, squeezing out extra throughput at the cost of significantly higher implementation complexity. This is advanced territory, but it’s worth knowing the term exists — it often comes up when comparing “why is this database so much faster for this one workload.”
High Availability, Replication & Distributed Locking
So far we’ve discussed locking inside a single database server. But modern systems run across many servers, data centres, and even continents. This raises a much harder question: how do you lock something when the data isn’t even on one machine?
13.1 Locking and Replication
Most production databases use a primary‑replica (leader‑follower) setup: writes go to one primary node, and locks are only managed there. Read replicas serve read‑only traffic and typically don’t need write locks at all — but they may lag slightly behind the primary (this is called replication lag, part of the “eventual consistency” trade‑off).
Fig 13.1 — Only the primary node manages write locks; replicas serve reads independently.
13.2 CAP Theorem in One Paragraph
The CAP theorem says a distributed system can only fully guarantee two out of three: Consistency (everyone sees the same data), Availability (the system always responds), and Partition tolerance (it keeps working even if network links break). Because network partitions are unavoidable in the real world, the real choice is between consistency and availability during a partition. Locking is fundamentally a consistency tool — it can reduce availability if a lock‑holder becomes unreachable.
13.3 Distributed Locks
Sometimes you need to lock something that isn’t even a database row — like making sure only one server in a cluster runs a scheduled job at a time. This is solved with distributed locking systems such as:
- Redis (via the SET NX + expiry pattern, or the Redlock algorithm) — fast, widely used, good for short‑lived locks.
- Apache ZooKeeper / etcd — built on the Raft or Paxos consensus algorithms, offering very strong guarantees, used by Kafka, Kubernetes, and many large‑scale systems.
- Database advisory locks — PostgreSQL offers pg_advisory_lock(), a lock that isn’t tied to any row, useful for app‑level coordination.
// Java example — acquiring a distributed lock in Redis using Jedis
public boolean acquireLock(Jedis jedis, String lockKey, String requestId, int ttlSeconds) {
// SET key value NX EX ttl -> only succeeds if key does not already exist
String result = jedis.set(lockKey, requestId, SetParams.setParams().nx().ex(ttlSeconds));
return "OK".equals(result);
}
public void releaseLock(Jedis jedis, String lockKey, String requestId) {
// Only delete if we still own it (avoids deleting someone else's lock after expiry)
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script, 1, lockKey, requestId);
}If the server holding a distributed lock crashes, it can never release the lock manually. A time‑to‑live (TTL) ensures the lock is automatically released after a set period, so the whole system doesn’t freeze forever waiting for a machine that will never come back.
13.4 Consensus Algorithms
When multiple nodes must agree on “who holds the lock right now,” they use consensus algorithms like Raft or Paxos. These algorithms let a cluster of machines agree on a single fact even if some machines crash or messages get lost — the same technology that lets etcd (used inside Kubernetes) safely coordinate leader election.
13.5 Failure Recovery
If a database crashes while holding locks, those locks simply vanish along with the crashed process’s in‑memory state. On restart, the database replays its Write‑Ahead Log (WAL) to redo committed work and undo uncommitted work — any transaction that was mid‑flight (and thus holding locks) is automatically rolled back, and its locks never need to be “manually” released because the in‑memory lock table itself was rebuilt from scratch.
Security Considerations
Locking isn’t just about correctness — it has real security implications too.
Denial of Service via Locking
A malicious or buggy client can open a long transaction and hold a lock on a critical row (e.g. an admin account) indefinitely, effectively freezing part of the application for everyone else. Databases guard against this with statement timeouts and lock wait timeouts.
Lock Timeout Settings
Most databases let you configure lock_timeout (PostgreSQL) or innodb_lock_wait_timeout (MySQL) so a stuck transaction is automatically killed rather than blocking forever.
Least Privilege
Application database users should only have the permissions they truly need. A compromised app account with unrestricted UPDATE rights can lock and modify far more than intended.
Audit & Isolation Level Abuse
Using a weaker isolation level than needed to “go faster” can silently expose sensitive data (like seeing another user’s uncommitted balance in Read Uncommitted mode).
Always set a reasonable statement and lock timeout in production. Without one, a single misbehaving request can hang an entire connection pool, cascading into a full outage — a very common real incident pattern.
Monitoring, Logging & Metrics
You can’t fix what you can’t see. Every production database team tracks locking‑related metrics closely.
| Metric | What It Tells You | Tooling Example |
|---|---|---|
| Lock wait time | How long transactions sit blocked waiting for locks | PostgreSQL pg_locks, MySQL performance_schema |
| Deadlock count | Frequency of deadlocks — a rising trend signals a design problem | MySQL SHOW ENGINE INNODB STATUS, Datadog, Grafana dashboards |
| Active vs. blocked queries | How much of your workload is stuck waiting right now | PostgreSQL pg_stat_activity |
| Long‑running transactions | Transactions that hold locks far longer than expected | Custom alerting on transaction duration > threshold |
-- PostgreSQL: find what's currently blocking what
SELECT blocked.pid AS blocked_pid,
blocked.query AS blocked_query,
blocking.pid AS blocking_pid,
blocking.query AS blocking_query
FROM pg_stat_activity blocked
JOIN pg_locks bl ON bl.pid = blocked.pid AND NOT bl.granted
JOIN pg_locks kl ON kl.locktype = bl.locktype AND kl.granted
JOIN pg_stat_activity blocking ON blocking.pid = kl.pid;In real production environments, teams wire these metrics into dashboards (Grafana, Datadog) and set alerts such as “page the on‑call engineer if deadlocks exceed 10/minute” or “alert if any transaction has held a lock for more than 30 seconds.” Distributed tracing tools (like OpenTelemetry) can also tag spans with lock‑wait time so engineers can see exactly which API call got stuck and why.
Deployment & Cloud Considerations
Cloud‑managed databases have changed how teams think about locking in production.
Amazon Aurora / RDS
Managed MySQL/PostgreSQL‑compatible engines still use standard row‑level locking internally, but Aurora’s storage layer separates compute from storage, so replicas apply changes faster with less lock contention on read replicas.
Google Cloud Spanner
A globally distributed database that uses a form of two‑phase locking combined with TrueTime (a globally synchronised clock) to offer strict serialisability across continents — a remarkable feat covered further in Section 21.
Serverless / Auto‑scaling Databases
Databases like Aurora Serverless or PlanetScale (built on Vitess) must carefully manage locks across sharded, auto‑scaling clusters, often abstracting sharding complexity away from developers.
Connection Pooling
Tools like PgBouncer or HikariCP limit the number of open connections, which indirectly reduces how many transactions can simultaneously compete for locks — an important tuning lever in the cloud.
When deploying to the cloud, always check your provider’s default lock timeout, isolation level, and deadlock retry behaviour — defaults sometimes differ from a self‑hosted install of the same database engine.
16.1 Backups, Disaster Recovery, and Locking
Taking a backup of a live, constantly‑changing database might sound like it would require locking the entire database for the whole duration — but modern backup tools avoid this. Techniques like consistent snapshots (relying on the same MVCC machinery from Section 8) let a backup process read a stable, point‑in‑time view of the data without blocking ongoing writes at all. Tools such as pg_basebackup for PostgreSQL or Percona XtraBackup for MySQL are specifically designed to take “hot backups” — full backups while the database keeps serving live traffic — precisely because they piggyback on the same versioning ideas that make MVCC reads non‑blocking. This is a major improvement over decades‑old backup strategies that genuinely did require freezing the whole database, which was unacceptable for any system that needs to run 24/7.
Disaster recovery plans should also account for locking behaviour during failover: when a primary node fails and a replica is promoted to take its place, any locks that existed only in the old primary’s memory are gone — in‑flight transactions are lost and must be retried by the application, which is another reason idempotent, retryable application logic (mentioned in Sections 7 and 20) is such a critical production habit, not just a deadlock‑specific fix.
Locking, Caching & Load Balancing
Locking concepts extend beyond the database itself into the rest of your architecture.
17.1 Cache Stampede and Locking
Imagine a popular cache entry (like a homepage’s “trending products” list) expires, and suddenly 10,000 requests hit the database at once to regenerate it. This is called a cache stampede. A common fix is to use a short‑lived distributed lock (like the Redis lock shown in Section 13) so only one request regenerates the cache while the rest wait briefly or serve stale data.
Fig 17.1 — A distributed lock prevents thousands of requests from hammering the database at once when a cache entry expires.
17.2 Load Balancers and “Sticky” Sessions
Load balancers themselves don’t hold database locks, but poor routing can worsen lock contention — e.g. always routing writes for the same customer to different app servers can increase the chance of overlapping transactions competing for the same row. Some systems intentionally route related writes (like all updates for one shopping cart) to the same backend instance to naturally serialise them before they even reach the database.
Locking in APIs & Microservices
In a microservices world, a single business transaction (like “place an order”) often spans multiple services and multiple databases — no single database lock can protect the whole flow.
18.1 The Problem with Distributed Transactions
Traditional locking (2PL) assumes one database. Across services, engineers historically used two‑phase commit (2PC), a distributed transaction protocol — but it’s slow and creates tight coupling between services (if one service is down, everyone is blocked).
18.2 The Modern Approach: Sagas
Most microservice architectures today avoid distributed locks entirely and instead use the Saga pattern: break the big transaction into a series of small, local transactions, each with a “compensating action” to undo it if a later step fails.
Fig 18.1 — A Saga chains local transactions with compensating actions instead of holding one giant distributed lock.
Within each individual service, however, ordinary row‑level locking (or optimistic locking) is still used at the database level — Sagas just remove the need for locks to span services.
18.3 Idempotency, Not Locks, for APIs
To prevent duplicate API calls (like a user double‑clicking “Pay Now”) from causing double charges, APIs commonly use idempotency keys rather than distributed locks: the client sends a unique key with the request, and the server checks (using a unique‑constraint or a short‑lived lock on that key) whether it has already processed that exact request.
Design Patterns & Anti-Patterns
A small number of named patterns keep showing up around locking because they answer recurring problems well. An equally small number of anti‑patterns keep showing up because they’re the shortcuts everyone is tempted to take.
19.1 Good Patterns
Lock ordering
Always acquire locks on multiple resources in a fixed, consistent order (e.g. always by ascending primary key) to eliminate a huge class of deadlocks.
Optimistic‑first, pessimistic fallback
Try optimistic locking by default; fall back to a pessimistic lock only for known “hot” resources (e.g. flash‑sale inventory).
Short transactions
Never do slow I/O (network calls, file writes) while holding a database lock.
Explicit timeouts
Always configure lock and statement timeouts rather than relying on defaults.
19.2 Anti-patterns to Avoid
Anti‑pattern
Locking rows in a different order across different code paths — a guaranteed deadlock generator.
Fix
Standardise on one canonical order everywhere (e.g. always the lower primary key first) and enforce it in code review.
Anti‑pattern
Holding a lock while calling an external API — if that API is slow, your lock (and everyone waiting on it) is stuck too.
Fix
Do the external call before or after the transaction; if the operations must be tied, use an outbox or saga pattern.
Anti‑pattern
Using SELECT * FOR UPDATE on huge tables without a WHERE clause — locks far more than intended.
Fix
Always scope FOR UPDATE queries with the tightest possible predicate; lock only the rows you actually need to change.
Anti‑pattern
Ignoring deadlock errors instead of retrying — leads to silent data loss or failed operations users never see reported correctly.
Fix
Wrap transactional operations in a bounded retry loop with backoff, so occasional deadlocks are absorbed transparently.
Anti‑pattern
Ultra‑long transactions (e.g. a nightly batch job wrapped in one giant transaction) that block regular traffic for hours.
Fix
Chunk the batch into small transactions (a few hundred to a few thousand rows each) with brief pauses to release locks.
Best Practices & Common Mistakes
The gap between a locking system that scales gracefully and one that collapses under load is usually not a technology gap — it’s a discipline gap. These are the habits successful teams share.
- 01Keep transactions as short as possible. Do all your business logic and computation outside the transaction; only wrap the actual database writes.
- 02Pick the loosest isolation level that’s still correct. Don’t reach for Serializable “just to be safe” — measure the real risk first.
- 03Always set lock and statement timeouts. A hung transaction should never be allowed to block the system forever.
- 04Access tables/rows in a consistent order across your entire codebase to prevent deadlocks.
- 05Prefer optimistic locking for low‑conflict, high‑read scenarios (like editing a user profile) and pessimistic locking for high‑conflict scenarios (like limited‑quantity ticket sales).
- 06Batch large writes into smaller chunks instead of one massive transaction touching millions of rows.
- 07Monitor lock waits and deadlocks continuously — treat rising numbers as an early warning sign, not just an incident trigger.
- 08Write idempotent retry logic in your application for deadlock errors — they will happen eventually at scale, and that’s expected, not a bug.
“Locking trades some concurrency for guaranteed correctness; the engineering skill is choosing the smallest, shortest‑lived lock that still keeps your data safe.”
Real-World & Industry Examples
Almost every recognisable digital experience is quietly powered by locking somewhere in the stack. Here’s a short tour of how large systems apply it in practice.
Ticket Sales (concert / airline booking)
Systems that sell limited‑quantity items under heavy simultaneous demand often use short pessimistic row locks (or Redis distributed locks) on the specific seat/ticket row to guarantee exactly one buyer wins, while everything else in the checkout flow (payment, confirmation emails) happens outside the lock.
E‑commerce Inventory (Amazon‑style)
Large retailers typically favour optimistic concurrency with versioned inventory counters combined with sharded “stock buckets,” so millions of shoppers can check out concurrently without every purchase queuing behind a single global lock.
Ride‑hailing Matching (Uber‑style)
Matching a rider to a nearby driver requires briefly locking a driver’s “available” status so two riders can’t be matched to the same driver at once — a classic short‑lived, high‑frequency pessimistic lock use case, often implemented with Redis or database row locks with tight timeouts.
Streaming Metadata (Netflix‑style)
Read‑heavy systems like content catalogs rely heavily on MVCC and caching rather than locking, since the vast majority of traffic is reads (browsing) rather than writes (catalog updates), so locks are reserved for the rare, tightly‑scoped writes.
Globally Distributed Databases (Google Spanner)
Spanner combines two‑phase locking with a globally synchronised clock system (TrueTime) to achieve external consistency across data centres worldwide — an advanced real‑world extension of the same core locking ideas taught in this tutorial.
Banking Core Systems
Traditional banking cores still rely heavily on strict pessimistic locking and Serializable isolation for account balance operations, prioritising absolute correctness over raw throughput, since a lost update literally means lost money.
Notice the pattern: the choice of locking strategy always follows the ratio of reads to writes and the cost of getting it wrong. High‑read systems lean on MVCC and caching; high‑conflict, high‑value writes (money, unique seats) lean on pessimistic locks or careful optimistic retries.
Frequently Asked Questions
A short set of the questions engineers, students, and interviewers ask most about database locking.
Is locking the same thing as a mutex in programming?
They’re conceptually the same idea (only one thread/transaction can hold it at a time), but a database lock also understands rows, tables, isolation levels, and transactions — a mutex in Java (like synchronized or ReentrantLock) only protects in‑memory data within a single application process, not data shared across many independent processes and machines.
Does NoSQL avoid locking entirely?
Not really — many NoSQL databases still use locking or MVCC internally (e.g. MongoDB uses document‑level locking with WiredTiger’s MVCC engine). What they usually avoid is multi‑document/multi‑row transactions spanning complex joins, which reduces the need for wide‑reaching locks.
Can I turn off locking completely?
You can weaken it by choosing Read Uncommitted isolation, but you can never fully turn it off — some minimal locking is always needed internally to keep the database’s own metadata consistent.
Why do deadlocks happen even in well‑written code?
Because they depend on unlucky timing between independent transactions, not just code logic. Even correct code with consistent lock ordering can occasionally deadlock under specific concurrent execution interleavings — that’s why retry logic is considered a best practice, not a failure of design.
What’s the difference between blocking and a deadlock?
Blocking is normal and temporary — one transaction waits for another to finish. A deadlock is a permanent stand‑off where waiting will never end without intervention.
Should I always use SELECT…FOR UPDATE to be safe?
No — overusing it hurts concurrency. Use it only when you genuinely need to prevent other transactions from reading/writing a row while you’re about to update it based on its current value.
What’s the difference between a lock and a latch again, in one line?
A lock protects logical data for the length of a transaction; a latch protects a physical in‑memory structure for a few microseconds. You’ll rarely interact with latches directly as an application developer, but the database engine relies on them constantly under the hood.
Why does my ORM (like Hibernate or Sequelize) sometimes throw an OptimisticLockException?
Because it’s implementing the version‑column pattern from Section 6 for you automatically. It read a row, tried to save your change, and discovered the version number had already changed — meaning someone else updated the same row first. Catch this exception, reload the fresh data, and retry.
Summary & Key Takeaways
Database locking is one of the oldest and still most essential ideas in computer science — the mechanism that lets many people share the same data without corrupting it. From a single hotel‑room key to globally distributed systems synchronising across continents with atomic clocks, the fundamental problem never changes: coordinate access so conflicting changes don’t collide.
Key Takeaways
- 01Locking prevents lost updates, dirty reads, non‑repeatable reads, and phantom reads by controlling concurrent access to data.
- 02Shared (read) locks can be held by many transactions at once; exclusive (write) locks can only be held by one.
- 03Two‑Phase Locking guarantees serialisability by never acquiring new locks after the first release.
- 04Pessimistic locking waits upfront; optimistic locking checks for conflicts only at write‑time — pick based on contention level.
- 05Deadlocks are detected via wait‑for graphs and resolved by rolling back a victim transaction; always design with consistent lock ordering.
- 06MVCC lets readers avoid blocking writers (and vice versa) by keeping multiple row versions — the backbone of modern high‑concurrency databases.
- 07Distributed systems extend locking with consensus algorithms (Raft, Paxos), distributed locks (Redis, ZooKeeper), and patterns like Sagas that avoid cross‑service locks entirely.
- 08Production success depends on monitoring lock waits/deadlocks, setting timeouts, keeping transactions short, and choosing the right isolation level for each workload.
“A good lock is a small, quiet promise: only one at a time — and you can always trust the answer, even when a million people are trying to change the same thing at once.”