Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 8

Distributed Locking: Approaches and Failure Modes

Recommended resources

Welcome back. In our previous lesson, we explored how to implement leader election using coordination services like ZooKeeper and etcd. We focused on the patterns that ensure a single node is in charge and, crucially, introduced the concept of fencing tokens to prevent "split-brain" scenarios where a stale leader could corrupt data.

Today, we'll tackle a related but distinct coordination problem: distributed locking. While leader election is about assigning a long-lived role to a single node, distributed locking is about providing temporary, exclusive access to a shared resource for any node that needs it. Your goal for this lesson is to compare the common approaches—using a database, Redis, or ZooKeeper—and, most importantly, to analyze their failure modes. For a senior engineering role, being able to articulate the subtle trade-offs and failure scenarios behind these patterns is a critical skill. We will build directly on the idea of fencing and see why it's a non-negotiable component of any lock that must guarantee correctness.

1. The Core Problem: Why Most Locks Are Broken

Before comparing implementations, we must understand the fundamental challenge. A distributed lock's purpose is to protect a "critical section" of code to ensure mutual exclusion. But what happens if the lock fails? This leads to the most important question you should ask when designing a system with locking, a distinction articulated powerfully by Martin Kleppmann.

How to do distributed locking

This article from Martin Kleppmann, a leading researcher in distributed systems, starts by framing the entire problem around the purpose of the lock.

Please read the first section, "What are you using that lock for?". Focus on the distinction between locks for efficiency and locks for correctness. This single concept will guide all our subsequent trade-off analyses.

As you've just read, the consequences of failure are everything.

  • Efficiency Locks: Prevent redundant work. If one fails, the cost is minimal (e.g., a report is generated twice).
  • Correctness Locks: Prevent data corruption. If one fails, the result is catastrophic (e.g., negative inventory, data loss).

The most insidious failures don't come from server crashes, but from the inherent unreliability of time in a distributed system. Your own practical experience with high-throughput APIs and managed runtimes like the JVM makes you aware of phenomena like garbage collection (GC) pauses. Let's see how such a pause can break a seemingly perfect lock.

How to do distributed locking

This section illustrates the classic failure mode for any lease-based lock.

Read the section "Protecting a resource with a lock". Pay close attention to the diagram showing how a long GC pause allows a client to perform a write after its lock has already expired and been granted to another client.

This scenario highlights that a lock service alone is insufficient. The client holding the lock is an unreliable actor. After a pause, it has no idea its lease has expired. The solution, which we touched upon in our last lesson, is the fencing token.

This state machine illustrates the lifecycle of a distributed lock. A client goes from `Acquiring` to `Held`. However, due to a `TTL Expiration` or a `Process Pause`, another client can acquire the lock. The first client, unaware, might still attempt operations, leading to a "Dual Ownership" temporal violation. This is precisely the problem that fencing tokens solve.

The article you've been reading explains this fix perfectly.

How to do distributed locking

Here, Kleppmann introduces the robust solution to the pause problem.

Read the section "Making the lock safe with fencing". The key takeaway is that the lock service must provide a strictly increasing token with each lock acquisition, and the resource being protected must validate this token, rejecting any request with a stale token.

With this foundational understanding of the problem (correctness vs. efficiency) and the solution (fencing), let's analyze the common implementations.


2. The Spectrum of Locking Solutions

We will evaluate three common approaches: using a database, using Redis, and using ZooKeeper. Each represents a different point on the spectrum of performance, complexity, and safety.

This table provides a high-level comparison of the three main approaches we'll discuss. It highlights the trade-offs in performance, fairness, and complexity that we are about to explore in detail.

2.1. Database Locks: Simplicity and ACID

If the resource you're protecting is a row in a relational database like PostgreSQL, the simplest solution is often to use the database's own locking mechanism.

  • Mechanism: You can use a transaction and a command like SELECT ... FOR UPDATE on a specific row. This places an exclusive lock on that row, causing other transactions trying to access it to block until the first transaction commits or rolls back.
  • Pros:
    • Simplicity: Leverages existing infrastructure and well-understood ACID guarantees. No extra services to manage.
    • Correctness: If the data and the lock are in the same system, the transactional guarantees provide strong safety.
  • Cons:
    • Performance: Database locks are relatively slow (disk I/O, transactional overhead) and can become a major bottleneck.
    • Scope: The lock is tied to the database. It can't be used to coordinate access to external resources (e.g., a third-party API or a file on S3).

Verdict: A good choice for correctness when the critical section involves operations within a single database and performance is not the primary driver.

2.2. Redis Locks: High Performance, High Debate

Given your experience with Redis for caching in your ride-booking project, you know it's incredibly fast. This makes it a popular choice for distributed locking, but its safety is a subject of intense debate.

Single-Instance Redis Lock

The modern, correct way to implement a lock on a single Redis instance is with a single atomic command:
SET lock_key unique_random_value NX PX 30000

  • NX: "Set only if not exists," ensuring mutual exclusion.
  • PX 30000: "Expire after 30000 milliseconds," providing a TTL to handle client crashes.
  • unique_random_value: A value to ensure a client only deletes its own lock on release.

This is a fantastic efficiency lock. It's extremely fast and simple. However, it's unsafe for correctness because a single Redis node is a single point of failure, and more subtly, it provides no fencing mechanism.

The Redlock Algorithm

To solve the single-point-of-failure problem, Redis's creator proposed Redlock, an algorithm that uses a majority vote across N (e.g., 5) independent Redis masters.

How Distributed Lock works | ft Redis | System Design

The video from the channel "ByteMonk" gives a clear, concise explanation of the Redlock algorithm's steps.

Watch the segment explaining the mechanics of Redlock, from the algorithm description. Focus on the core idea: a client must acquire the lock on a majority of instances within the lock's validity time.

Redlock seems more fault-tolerant, but does it solve the fundamental safety problems we discussed? This question sparked one of the most famous debates in distributed systems.

How to do distributed locking

Kleppmann's article continues with a devastating critique of Redlock's safety.

Please read the following sections: "Using time to solve consensus": This explains how Redlock relies on dangerous assumptions about timing (a synchronous system model). "Breaking Redlock with bad timings": This provides concrete examples of how clock jumps or GC pauses can cause Redlock to grant a lock to two different clients, violating mutual exclusion. "Conclusion": This summarizes the argument against Redlock for correctness-critical applications.

The consensus in the systems community is clear: Redlock adds significant complexity but fails to provide the safety required for correctness locks, primarily because it lacks a fencing token mechanism and is vulnerable to timing issues. It is an attempt to solve server failures (crashes) while ignoring the more insidious client-side failures (pauses).

2.3. ZooKeeper Locks: Correctness and Order

When you need a provably correct distributed lock, you turn to a consensus-based system like ZooKeeper. As we saw in the previous lesson, ZooKeeper is built for reliability.

The lock recipe in ZooKeeper is an elegant composition of its core features.

A Systematic Analysis of Distributed Locking: A Comparative Study ...

This article provides an excellent, detailed walkthrough of the ZooKeeper lock recipe.

Please read the entire section "IV. Strongly Consistent Locking with Zookeeper: Reliability and Order," from "IV. Strongly Consistent Locking with Zookeeper". Focus on how ephemeral, sequential znodes, and targeted watches combine to create a lock that is: Fault-tolerant: Ephemeral nodes handle client crashes automatically. Fair: Sequential nodes create a FIFO queue, preventing starvation. Efficient: Watching only the predecessor avoids the "thundering herd" problem.

Most importantly, ZooKeeper solves the fencing problem. The ZAB protocol that powers ZooKeeper assigns a unique, monotonically increasing transaction ID (zxid) to every state change. When your client creates its lock znode, the creation zxid serves as a perfect fencing token. This is the fundamental advantage that makes ZooKeeper-based locks suitable for correctness-critical applications where Redlock is not.


3. Synthesis and Interview Articulation

For a senior SDE interview, you must be able to synthesize this information and articulate the trade-offs clearly.

Feature Database Lock (e.g., SELECT ... FOR UPDATE) Redis (Single-Instance) Redlock ZooKeeper / etcd
Primary Strength Simplicity, ACID guarantees Extreme Performance Attempted Fault Tolerance Strong Consistency & Safety
Safety under Pauses Safe (within DB transaction) Unsafe Unsafe (vulnerable to timing, no fencing) Safe (provides native fencing tokens)
Fencing Mechanism N/A (implicit in transaction) No No Yes (zxid in ZK, mod_revision in etcd)
Performance Low (Disk + ACID overhead) Very High (in-memory) High Moderate (requires consensus & disk persistence)
Fairness (FIFO) Database-dependent No (race condition) No (race condition) Yes (via sequential nodes)
Best For Correctness (data is in the DB) Efficiency (caching, rate limiting) Disputed; single-instance is often better/simpler. Correctness (leader election, critical resource locking)

When asked about distributed locking in an interview:

  1. Start with the clarifying question: "Is this lock for efficiency or correctness?"
  2. For efficiency: Propose a single-instance Redis lock using the atomic SET ... NX PX command. Explain that it's fast and simple, and the occasional failure is acceptable.
  3. For correctness: Propose a ZooKeeper/etcd-based lock. Explain that its consensus-based foundation provides the required safety guarantees. Crucially, describe the fencing token mechanism (zxid/mod_revision) and explain how the resource being protected will use it to reject stale requests from paused clients. This demonstrates a deep understanding of distributed system failure modes.

Conclusion

In this lesson, we have dissected the problem of distributed locking, moving from the fundamental "efficiency vs. correctness" dichotomy to a detailed analysis of specific implementations.

Key Takeaways:

  • A lock's purpose—efficiency or correctness—is the primary factor in choosing an implementation.
  • Simple time-based leases are insufficient for safety due to unbounded process pauses and network delays.
  • Fencing tokens are the essential mechanism for building correct distributed systems, shifting the final authority to the resource server.
  • Redis is excellent for high-performance efficiency locks. The Redlock algorithm, despite its aims, is not considered safe for correctness-critical applications.
  • ZooKeeper/etcd provide robust, provably correct locks with built-in support for fairness and fencing, making them the standard choice for ensuring correctness.

Our discussion has consistently revolved around the trade-offs between consistency and other system properties like performance (latency). In our next lesson, we will introduce the PACELC theorem, a formal model that extends the CAP theorem to help us reason more precisely about the trade-off between latency and consistency during normal operation, which is exactly the choice between a Redis-based and a ZooKeeper-based system.