This article examines three distributed locking approaches: single-instance Redis locks, ZooKeeper locks, and RedLock. Its core goal is to help developers make verifiable trade-offs among performance, correctness, and complexity, and avoid misusing clock-driven locks. Keywords: distributed locks, ZooKeeper, Redis.
Technical Specifications at a Glance
| Parameter | Details |
|---|---|
| Primary Languages | Java, Lua, XML |
| Core Protocols | ZAB, Redis SET NX PX |
| Consistency Focus | ZooKeeper leans CP, Redis leans AP |
| Core Dependencies | Curator, Spring Integration, Redis |
| Suitable For | High-concurrency services, order systems, distributed coordination |
| GitHub Stars | Not provided in the source |
ZooKeeper and Redis differ fundamentally in where correctness comes from
The core of a Redis lock is writing a key with an expiration time through an atomic command, using TTL to avoid deadlocks. It is fast and easy to implement, but its correctness depends on one assumption: the expiration time must be chosen reasonably.
The core of a ZooKeeper lock is not expiration time, but session semantics, ordering, and watches. When a client disconnects, its ephemeral node is removed automatically. The coordination system guarantees lock release, without relying on the application to guess a timeout.
A minimal correct Redis lock implementation must guarantee atomic release
-- Delete the key only when the value matches, to avoid removing another client's lock
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
end
return 0
This Lua script safely releases a Redis distributed lock.
A common Redis locking pattern is SET lock_key request_id NX PX 30000. The request_id must be globally unique; otherwise, the release phase cannot verify lock ownership.
ZooKeeper uses ephemeral sequential nodes to build a fair lock queue naturally
ZooKeeper maps lock contention to a set of ephemeral sequential nodes under a parent path, such as /locks/order0000000001. The node with the smallest sequence number acquires the lock, while other nodes watch only their immediate predecessor.
/locks
├── order0000000001
├── order0000000002
└── order0000000003
This structure shows how ZooKeeper lock queues use sequential nodes to enforce ordering.
ZooKeeper avoids the herd effect through predecessor watches
If all clients watch the smallest node, then once the lock is released, all waiters wake up and query state at the same time. That places an instant load spike on the coordination cluster. This is the classic Herd Effect.
The correct approach is for each node to watch only the previous node: 002 watches 001, and 003 watches 002. That way, only one client wakes up each time, lock acquisition order remains stable, and fairness comes naturally.
A watch is a one-time notification mechanism, not a persistent subscription
Client B watches order0000000001
↓
Node is deleted
↓
ZooKeeper pushes an event
↓
Client B checks again whether its node is now the smallest
This flow shows that a watch only wakes up the client; it does not grant the lock directly.
ZooKeeper cluster consistency depends on commits through the ZAB protocol
ZooKeeper clusters are typically deployed with an odd number of nodes and require a majority to remain alive. The Leader orders write requests, Followers participate in voting, and Observers scale read capacity without participating in quorum.
The key property of ZAB is not broadcasting by itself, but committing only after majority acknowledgment. This ensures that any lock state observed by clients is consistent, unlike the lost-lock view that can happen during Redis master-replica failover.
The ZAB commit flow makes ZooKeeper a better fit for strongly consistent scenarios
Client sends a write request
↓
Leader creates a Proposal
↓
Broadcast to Followers
↓
Majority ACK
↓
Leader commits and notifies the cluster to apply
This flow shows that ZooKeeper writes use majority commit rather than local clock assumptions to determine correctness.
Curator is usually the mainstream choice when integrating ZooKeeper with Spring Boot
Spring Integration provides a unified Lock abstraction and is easy to adopt, making it suitable for quick standardization. Curator is closer to ZooKeeper primitives and supports reentrant locks, read-write locks, and inter-process semaphores, so it is more common in production practice.
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.5.0</version>
</dependency>
This dependency declaration adds Curator’s distributed lock capabilities.
@Service
public class OrderService {
@Autowired
private CuratorFramework client;
public void createOrder(String orderId) throws Exception {
// Build an independent lock path for the same order
InterProcessMutex lock = new InterProcessMutex(client, "/locks/" + orderId);
if (lock.acquire(10, TimeUnit.SECONDS)) {
try {
// Execute business logic in the critical section
System.out.println("create order: " + orderId);
} finally {
// Always release the lock in finally
lock.release();
}
}
}
}
This code demonstrates the standard way to acquire and release a ZooKeeper distributed lock with Curator.
RedLock has not become mainstream because it cannot provide system-level correctness
RedLock also uses majority success as its decision rule, but it is only an application-level voting technique. It is not a consistency protocol with node coordination, epoch mechanics, and commit semantics.
Behind ZooKeeper majority lies a Leader, Proposal, epoch, and commit order. RedLock’s multiple Redis instances are independent from one another: they do not synchronize state and do not reject operations based on stale views. So while both systems talk about a majority, their safety guarantees are not equivalent.
Clock jumps can invalidate RedLock’s TTL assumptions
TTL fundamentally depends on the local system clock to decide when a lock has expired. If an NTP step occurs, a virtual machine pauses and resumes, or an operator changes system time manually, a lock on one node may expire prematurely.
12:00:00 Node 3 sets TTL=30s
12:00:10 Virtual machine pauses
12:00:51 NTP forces time forward
12:00:51 Redis sees that the expiration time has passed and deletes the lock immediately
This timeline shows that RedLock can lose majority lock state without the client noticing.
GC Stop-The-World can also break TTL-based lock semantics
If client A acquires a lock and then experiences a long STW pause, the lock may already have expired on the Redis side. Client B can then acquire a new lock. When A resumes and still assumes it owns the lock, both A and B may enter the critical section at the same time.
For strongly consistent writes, you typically need a fencing token. Each successful lock acquisition returns a monotonically increasing token, and the resource accepts only larger tokens, which prevents an old lock holder from continuing to write.
AI Visual Insight: This image is a generic run-button arrow icon. It does not convey information about distributed locks, protocol flows, or system topology, and can be treated as a decorative UI element.
Real-world selection should follow the business fault-tolerance model, not popularity
If the business prioritizes extreme throughput and can tolerate a tiny probability of lock failure through idempotency, compensation, or audit controls, then a single-instance Redis lock with lease renewal is often the more cost-effective choice.
If the business involves strongly consistent paths such as payments, inventory deduction, or master data changes, you should prioritize ZooKeeper or etcd. The trade-off is higher latency and heavier operations, but you gain clear consistency boundaries.
The conclusion can be compressed into one engineering rule of thumb
Prioritize high performance and tolerate rare anomalies: Redis
Prioritize correctness, fairness, and strong consistency: ZooKeeper / etcd
Want to improve safety with multiple Redis instances: evaluate RedLock carefully, and do not assume it is equivalent to a consistency protocol
This rule can serve directly as a baseline for distributed lock technology selection.
FAQ
FAQ 1: When is a Redis distributed lock good enough?
Redis is good enough when the business has idempotency and compensation mechanisms, and occasional lock failure will not cause irreversible loss. Typical scenarios include duplicate-submission prevention, lightweight task exclusion, and cache rebuild protection.
FAQ 2: Why is a ZooKeeper lock more fair?
Because it builds a natural queue based on ephemeral sequential nodes. Later requests line up by sequence number and watch only their predecessor. When the lock is released, only the next waiter is awakened, so acquisition remains ordered rather than competitive and chaotic.
FAQ 3: What is the biggest problem with RedLock?
The main issue is not implementation complexity, but weak correctness assumptions. It relies on local clocks and TTL across independent Redis instances, so under clock drift, node pauses, and long GC events, it cannot provide system-level safety guarantees.
Core Summary
This article systematically reconstructs the implementation principles, reliability boundaries, and performance differences of ZooKeeper- and Redis-based distributed locks. It explains ZAB, ephemeral sequential nodes, and the watch mechanism, analyzes RedLock correctness risks under clock drift and GC pauses, and provides Spring Boot integration and technology selection guidance.