CountDownLatch Implementation Comparison: redlock4j vs Redisson¶
This document provides a detailed technical comparison of the CountDownLatch implementations in redlock4j and Redisson.
Overview¶
Both libraries implement distributed countdown latches for coordinating multiple threads/processes, but they use different approaches for counting and notification.
Purpose & Use Case¶
redlock4j RedlockCountDownLatch¶
Purpose: Distributed countdown latch with quorum-based consistency
Use Case: Coordinating distributed processes with strong consistency requirements
// Create latch waiting for 3 operations
RedlockCountDownLatch latch = manager.createCountDownLatch("startup", 3);
// Worker threads
new Thread(() -> {
initializeService1();
latch.countDown();
}).start();
new Thread(() -> {
initializeService2();
latch.countDown();
}).start();
new Thread(() -> {
initializeService3();
latch.countDown();
}).start();
// Main thread waits
latch.await(); // Blocks until count reaches 0
System.out.println("All services initialized!");
Redisson RedissonCountDownLatch¶
Purpose: Distributed countdown latch with pub/sub notifications
Use Case: High-performance coordination with single Redis instance
RCountDownLatch latch = redisson.getCountDownLatch("startup");
latch.trySetCount(3);
// Worker threads
new Thread(() -> {
initializeService1();
latch.countDown();
}).start();
// ... more workers
// Main thread waits
latch.await();
System.out.println("All services initialized!");
Architecture & Data Model¶
redlock4j¶
Design: Counter with quorum-based reads/writes and pub/sub
Data Structure:
{latchKey} = {count} (counter on each node)
{latchKey}:channel = (pub/sub channel) (notification channel)
Key Characteristics: - Counter replicated across all nodes - Quorum-based count reads - Quorum-based decrements - Atomic per-node decrement + publish (single Lua script) - Local latch for waiting - Automatic expiration (10x lock timeout)
Architecture:
RedlockCountDownLatch
├─ latchKey (counter key)
├─ channelKey (pub/sub)
├─ LockExecutionStrategy (node fan-out + quorum)
├─ CountDownLatch localLatch (for waiting)
├─ AtomicBoolean subscribed
└─ Atomic DECR + PUBLISH (decrAndPublishIfZero)
Redisson¶
Design: Single counter with pub/sub and Lua scripts
Data Structure:
{latchKey} = {count} (single counter)
redisson_countdownlatch__channel__{latchKey} = (pub/sub channel)
Key Characteristics: - Single counter (not replicated) - Atomic Lua scripts - Pub/sub for notifications - Async/reactive support - No automatic expiration
Architecture:
RedissonCountDownLatch
├─ latchKey (counter key)
├─ channelName (pub/sub)
├─ CountDownLatchPubSub (notification handler)
├─ Lua scripts for atomicity
└─ Async futures
Initialization¶
redlock4j¶
Initialization: Automatic when created via the manager factory. Users call
manager.createCountDownLatch(latchKey, count); the manager wires the internal
drivers and execution strategy and seeds the counter on all nodes with a long
expiration (10x lock timeout).
// Simplified/conceptual pseudocode of the internal setup
this.latchKey = latchKey;
this.channelKey = latchKey + ":channel";
this.initialCount = count;
this.localLatch = new CountDownLatch(1);
// Seed the counter on the nodes via the execution strategy,
// with a long expiration (10x lock timeout)
executionStrategy.executeOnNodes(driver -> {
driver.setex(latchKey, String.valueOf(count),
config.getDefaultLockTimeout().toMillis() * 10);
return true;
});
Count Down Operation¶
redlock4j¶
Algorithm: Atomic per-node decrement-and-publish, fanned out via the execution strategy
public void countDown() {
// Fan out to the appropriate nodes via the execution strategy
int successCount = executionStrategy.executeOnNodes(driver -> {
try {
// Atomic: decrement and publish "zero" if the count hits zero,
// in a single Lua script (decrAndPublishIfZero)
long count = driver.decrAndPublishIfZero(latchKey, channelKey, "zero");
return true;
} catch (Exception e) {
logger.debug("Failed to decrement latch count on {}", driver.getIdentifier());
return false;
}
});
if (executionStrategy.isSuccessful(successCount)) {
logger.debug("Successfully decremented latch {} on {} nodes", latchKey, successCount);
} else {
logger.warn("Failed to decrement latch {} on sufficient nodes", latchKey);
}
}
Where decrAndPublishIfZero runs this Lua script atomically on each node:
local count = redis.call("DECR", KEYS[1])
if count <= 0 then
redis.call("PUBLISH", KEYS[2], ARGV[1])
end
return count
Characteristics: - Atomic DECR + PUBLISH per node (single Lua script) - Fan-out and quorum check delegated to the execution strategy - Decrement and zero-notification cannot interleave on a node
Redis Operations (M nodes):
- M × decrAndPublishIfZero Lua script (DECR, plus PUBLISH when zero)
Redisson¶
Algorithm: Atomic Lua script with notification
-- countDownAsync
local v = redis.call('decr', KEYS[1]);
if v <= 0 then
redis.call('del', KEYS[1])
end;
if v == 0 then
redis.call(ARGV[2], KEYS[2], ARGV[1])
end;
Characteristics: - Atomic decrement + delete + publish - Single operation - Deletes key when zero - Publishes ZERO_COUNT_MESSAGE
Redis Operations: - 1 Lua script execution
Await Operation¶
redlock4j¶
Algorithm: Subscribe + poll with local latch
public boolean await(Duration timeout) throws InterruptedException {
// Subscribe to notifications
subscribeToNotifications();
// Check if already zero
long currentCount = getCount();
if (currentCount <= 0) {
return true;
}
// Wait on local latch (released by pub/sub notification)
return localLatch.await(timeout.toMillis(), TimeUnit.MILLISECONDS);
}
private void subscribeToNotifications() {
if (subscribed.compareAndSet(false, true)) {
new Thread(() -> {
// Subscribe on a single node
subscriptionDriver.subscribe(new MessageHandler() {
@Override
public void onMessage(String channel, String message) {
if ("zero".equals(message)) {
localLatch.countDown(); // Release waiters
}
}
}, channelKey);
}).start();
}
}
public long getCount() {
List<Long> results = new ArrayList<>();
// Read the count from the appropriate nodes via the execution strategy
executionStrategy.executeOnNodes(driver -> {
String countStr = driver.get(latchKey);
if (countStr != null) {
synchronized (results) {
results.add(Long.parseLong(countStr));
}
}
return true;
});
if (!results.isEmpty()) {
long total = results.stream().mapToLong(Long::longValue).sum();
return Math.max(0, total / results.size()); // Average across nodes
}
return 0; // Conservative fallback
}
Characteristics: - Subscribe to single driver (first one) - Check count via quorum read - Wait on local CountDownLatch - Pub/sub releases local latch - Average count across nodes
Redis Operations (M nodes):
- 1 × SUBSCRIBE
- M × GET (check count)
Redisson¶
Algorithm: Subscribe + async polling
public void await() throws InterruptedException {
if (getCount() == 0) {
return;
}
CompletableFuture<RedissonCountDownLatchEntry> future = subscribe();
RedissonCountDownLatchEntry entry = future.join();
try {
while (getCount() > 0) {
entry.getLatch().await(); // Wait for notification
}
} finally {
unsubscribe(entry);
}
}
private CompletableFuture<RedissonCountDownLatchEntry> subscribe() {
return pubSub.subscribe(getEntryName(), getChannelName());
}
public long getCount() {
return commandExecutor.writeAsync(
getRawName(), LongCodec.INSTANCE,
RedisCommands.GET_LONG, getRawName()
).join();
}
Characteristics: - Subscribe via pub/sub service - Poll count in loop - Semaphore-based waiting - Async futures - Unsubscribe when done
Redis Operations:
- 1 × SUBSCRIBE
- N × GET (polling)
Reset Operation¶
redlock4j¶
Reset: Supported (non-standard)
public void reset() {
// Delete the existing latch on the appropriate nodes
executionStrategy.executeOnNodes(driver -> {
driver.del(latchKey);
return true;
});
// Reset local state
localLatch = new CountDownLatch(1);
subscribed.set(false);
// Reinitialize with the original count
initializeLatch(initialCount);
}
Characteristics: - Deletes on all nodes - Resets local latch - Reinitializes with original count - Not atomic - Can cause race conditions
Redisson¶
Reset: Supported via trySetCount()
Characteristics: - Must explicitly delete first - Then set new count - Two separate operations - Not atomic - Publishes notifications
Performance Comparison¶
redlock4j¶
Count Down (M nodes):
- M × decrAndPublishIfZero Lua script (atomic DECR, plus PUBLISH when zero)
- Total: M script executions
Await:
- 1 × SUBSCRIBE
- M × GET (quorum read)
- Total: M+1 operations
Complexity: O(M) per operation
Latency: - Higher due to quorum - Multiple round trips - Pub/sub to all nodes
Redisson¶
Count Down: - 1 Lua script execution - Total: 1 operation
Await:
- 1 × SUBSCRIBE
- N × GET (polling)
- Total: N+1 operations
Complexity: O(1) for countDown, O(N) for await
Latency: - Lower for single instance - Single round trip for countDown - Polling overhead for await
Measured Performance¶
Benchmark: 5 clients, count=5, 50 ms work per cycle, 3-node Redis 7 cluster, 60 s measurement (full methodology in Architecture › Performance Analysis).
| Implementation | Ops/s | p50 (ms) | p99 (ms) | mean (ms) | success |
|---|---|---|---|---|---|
| Redisson | 59.02 | 17.17 | 22.57 | 16.91 | 100 % |
| redlock4j-singlenode | 58.19 | 16.17 | 19.87 | 15.67 | 100 % |
| redlock4j-3node | 59.91 | 15.29 | 19.89 | 15.18 | 100 % |
Reading the numbers: redlock4j-3node matches Redisson on throughput while delivering slightly lower p50/p99/mean latency. Quorum overhead is fully amortized by the parallel multi-node I/O — coordination primitives benefit from multi-node distribution rather than paying for it.
Safety & Correctness¶
redlock4j¶
Safety Guarantees: - Quorum-based consistency - Survives minority node failures - Count averaged across nodes - Automatic expiration - No single point of failure
Potential Issues: - Higher latency - More network overhead - Subscribe to single node only - Count averaging may be inaccurate - Reset not atomic
Consistency Model:
Count decremented if:
- Quorum of nodes decremented
- Average count used for reads
Notification sent if:
- The decrement drives a node's count to zero
- Decrement + publish are atomic on that node (single Lua script)
Redisson¶
Safety Guarantees: - Atomic operations (Lua scripts) - Accurate count - Pub/sub notifications - Async/reactive support - Low latency
Potential Issues: - Single point of failure - No quorum mechanism - No automatic expiration - Polling in await loop - Reset not atomic
Consistency Model:
Count decremented if:
- Atomic Lua script succeeds
- Single instance
Notification sent if:
- Count reaches exactly zero
- Atomic with decrement
Feature Comparison Table¶
| Feature | redlock4j | Redisson |
|---|---|---|
| Data Model | Counter on all nodes | Single counter |
| Quorum | Yes | No |
| Fault Tolerance | Survives minority failures | Single point of failure |
| Initialization | Automatic via manager factory | Explicit via trySetCount() |
| Expiration | Automatic (10x timeout) | No automatic expiration |
| Count Accuracy | Average across nodes | Exact |
| Atomicity | Atomic per-node DECR + PUBLISH (Lua script) | Atomic (Lua script) |
| Subscription | Single node | Managed pub/sub service |
| Reset | Supported (non-standard) | Supported via delete + trySetCount |
| Async Support | No | Yes |
| Reactive Support | No | Yes |
| Performance | O(M) | O(1) for countDown |
| Latency | Higher | Lower |
| Network Overhead | High | Low |
Use Case Comparison¶
redlock4j RedlockCountDownLatch¶
Best For: - Distributed systems requiring quorum-based safety - Coordination with strong consistency - Multi-master Redis setups - Fault-tolerant coordination - Automatic expiration needed
Example Scenarios:
// Distributed service startup coordination
RedlockCountDownLatch startupLatch = manager.createCountDownLatch("app:startup", 5);
// Batch job coordination
RedlockCountDownLatch batchLatch = manager.createCountDownLatch("batch:job:123", 100);
// Multi-stage workflow
RedlockCountDownLatch stageLatch = manager.createCountDownLatch("workflow:stage1", 10);
Redisson RedissonCountDownLatch¶
Best For: - Single Redis instance deployments - High-throughput coordination - Applications needing async/reactive APIs - Scenarios requiring exact count - Low-latency requirements
Example Scenarios:
// High-performance service coordination
RCountDownLatch startupLatch = redisson.getCountDownLatch("app:startup");
startupLatch.trySetCount(5);
// Async coordination
RCountDownLatch asyncLatch = redisson.getCountDownLatch("async:task");
asyncLatch.trySetCount(10);
RFuture<Void> future = asyncLatch.awaitAsync();
// Reusable latch
RCountDownLatch reusableLatch = redisson.getCountDownLatch("reusable");
reusableLatch.trySetCount(3);
// ... use it
reusableLatch.delete();
reusableLatch.trySetCount(5); // Reuse
Recommendations¶
Choose redlock4j RedlockCountDownLatch when:¶
- Need quorum-based distributed consistency
- Require fault tolerance (multi-master)
- Automatic expiration is important
- Can tolerate higher latency
- Count averaging is acceptable
Choose Redisson RedissonCountDownLatch when:¶
- Single Redis instance is acceptable
- Need high throughput / low latency
- Require exact count tracking
- Need async/reactive APIs
- Want atomic operations
- Explicit initialization preferred
Migration Considerations¶
From Redisson to redlock4j¶
// Before (Redisson)
RCountDownLatch latch = redisson.getCountDownLatch("startup");
latch.trySetCount(3);
latch.await();
// After (redlock4j)
RedlockCountDownLatch latch = manager.createCountDownLatch("startup", 3);
latch.await();
Benefits: - Quorum-based safety - Fault tolerance - Automatic expiration
Considerations: - Higher latency - Count is averaged - No async support
From redlock4j to Redisson¶
// Before (redlock4j)
RedlockCountDownLatch latch = manager.createCountDownLatch("startup", 3);
// After (Redisson)
RCountDownLatch latch = redisson.getCountDownLatch("startup");
latch.trySetCount(3);
Benefits: - Lower latency - Exact count - Async/reactive support - Atomic operations
Considerations: - Single point of failure - Must initialize explicitly - No automatic expiration
Conclusion¶
Both implementations provide distributed countdown latches with different trade-offs:
redlock4j RedlockCountDownLatch: - Quorum-based with fault tolerance - Automatic initialization and expiration - Higher latency but survives failures - Count averaged across nodes - Best for multi-master setups requiring strong consistency
Redisson RedissonCountDownLatch: - Atomic Lua scripts with exact counting - Lower latency but single point of failure - Pub/sub notifications - Async/reactive support - Best for high-throughput single-instance deployments
Choose based on your specific requirements: - Distributed consistency & fault tolerance → redlock4j RedlockCountDownLatch - High throughput & low latency → Redisson RedissonCountDownLatch - Exact count tracking → Redisson RedissonCountDownLatch - Automatic expiration → redlock4j RedlockCountDownLatch