Semaphore Implementation Comparison: redlock4j vs Redisson¶
This document provides a detailed technical comparison of the Semaphore implementations in redlock4j and Redisson.
Overview¶
Both libraries implement distributed semaphores to limit concurrent access to resources, but they use fundamentally different approaches and data structures.
Purpose & Use Case¶
redlock4j RedlockSemaphore¶
Purpose: Distributed semaphore with quorum-based safety guarantees
Use Case: Rate limiting and resource pooling with strong consistency requirements
// Create a semaphore with 5 permits
RedlockSemaphore semaphore = manager.createSemaphore("api-limiter", 5);
// Acquire a permit
if (semaphore.tryAcquire(Duration.ofSeconds(5))) {
try {
// Perform rate-limited operation
callExternalAPI();
} finally {
semaphore.release();
}
}
Redisson RedissonSemaphore¶
Purpose: Distributed semaphore with pub/sub notification
Use Case: General-purpose semaphore with async support and efficient waiting
RSemaphore semaphore = redisson.getSemaphore("api-limiter");
semaphore.trySetPermits(5);
// Acquire a permit
if (semaphore.tryAcquire(5, TimeUnit.SECONDS)) {
try {
// Perform rate-limited operation
callExternalAPI();
} finally {
semaphore.release();
}
}
Architecture & Data Model¶
redlock4j¶
Design: Individual permit keys with quorum-based acquisition
Data Structure:
{semaphoreKey}:permit:{permitId1} = {permitId1} (TTL: lockTimeout)
{semaphoreKey}:permit:{permitId2} = {permitId2} (TTL: lockTimeout)
{semaphoreKey}:permit:{permitId3} = {permitId3} (TTL: lockTimeout)
...
Key Characteristics:
- Each permit is a separate Redis key
- Permit acquisition uses SET NX (same as lock)
- Quorum required for each permit
- No central counter
- Thread-local state tracking
Architecture:
RedlockSemaphore
├─ semaphoreKey (base key)
├─ maxPermits (configured limit)
├─ List<RedisDriver> (quorum-based)
└─ ThreadLocal<PermitState>
├─ List<String> permitIds
├─ acquisitionTime
└─ validityTime
Redisson¶
Design: Single counter with pub/sub notification
Data Structure:
{semaphoreKey} = {availablePermits} (integer counter)
redisson_sc:{semaphoreKey} = (pub/sub channel)
Key Characteristics:
- Single Redis key stores available permit count
- Uses DECRBY for acquisition, INCRBY for release
- Pub/sub for efficient waiting
- No quorum mechanism
- Async/reactive support
Architecture:
RedissonSemaphore
├─ semaphoreKey (counter key)
├─ channelName (pub/sub)
├─ SemaphorePubSub (notification)
└─ No thread-local state
Permit Acquisition Algorithm¶
redlock4j¶
Algorithm: Create individual permit keys with quorum
private SemaphoreResult attemptAcquire(int permits) {
List<String> permitIds = new ArrayList<>();
Instant startTime = Instant.now();
// 1. For each permit needed
for (int i = 0; i < permits; i++) {
String permitId = generateLockValue();
String permitKey = semaphoreKey + ":permit:" + permitId;
// 2. Acquire the permit key on the nodes via the execution strategy
// (fan-out + quorum, same path as a standard lock)
LockResult result = executionStrategy.acquireLock(permitKey, permitId,
config.getDefaultLockTimeout().toMillis());
// 3. Check the result for this permit
if (result.isAcquired()) {
permitIds.add(permitId);
} else {
// Failed - rollback all permits
releasePermits(permitIds);
return new SemaphoreResult(false, 0, new ArrayList<>());
}
}
// 4. Compute validity via the strategy (handles single- vs multi-node drift)
Duration elapsed = Duration.between(startTime, Instant.now());
long validityTime = executionStrategy.calculateValidityTime(
config.getDefaultLockTimeout().toMillis(), elapsed.toMillis());
boolean acquired = permitIds.size() == permits && validityTime > 0;
if (!acquired) {
releasePermits(permitIds);
return new SemaphoreResult(false, 0, new ArrayList<>());
}
return new SemaphoreResult(true, validityTime, permitIds);
}
Redisson¶
Algorithm: Atomic counter decrement with Lua script
-- tryAcquireAsync0
local value = redis.call('get', KEYS[1]);
if (value ~= false and tonumber(value) >= tonumber(ARGV[1])) then
local val = redis.call('decrby', KEYS[1], ARGV[1]);
return 1;
end;
return 0;
Flow: 1. Get current permit count 2. Check if enough permits available 3. Atomically decrement counter 4. Return success/failure
Redis Operations (for N permits): - 1 Lua script execution (atomic) - If waiting: pub/sub subscription + notifications
Waiting Mechanism:
public void acquire(int permits) throws InterruptedException {
if (tryAcquire(permits)) {
return; // Got it immediately
}
// Subscribe to notifications
CompletableFuture<RedissonLockEntry> future = subscribe();
RedissonLockEntry entry = future.get();
try {
while (true) {
if (tryAcquire(permits)) {
return; // Got it
}
// Wait for notification
entry.getLatch().acquire();
}
} finally {
unsubscribe(entry);
}
}
Permit Release Algorithm¶
redlock4j¶
Algorithm: Delete individual permit keys
private void releasePermits(List<String> permitIds) {
for (String permitId : permitIds) {
String permitKey = semaphoreKey + ":permit:" + permitId;
// Delete on the appropriate nodes via the execution strategy
executionStrategy.executeOnNodes(driver -> {
driver.deleteIfValueMatches(permitKey, permitId);
return true;
});
}
}
Characteristics: - Delete each permit key individually - No notification to waiting threads - Waiting threads discover via polling - Quorum-based deletion
Redis Operations (for N permits on M nodes):
- N × M DELETE operations
Redisson¶
Algorithm: Atomic counter increment with pub/sub notification
-- releaseAsync
local value = redis.call('incrby', KEYS[1], ARGV[1]);
redis.call(ARGV[2], KEYS[2], value);
Characteristics: - Increment counter atomically - Publish notification to channel - Waiting threads wake up immediately - Single Redis operation
Redis Operations (for N permits):
- 1 Lua script execution
- 1 PUBLISH to channel
Notification Flow:
Thread 1: acquire() → blocks → subscribes to channel
Thread 2: release() → INCRBY + PUBLISH
Thread 1: receives notification → wakes up → tryAcquire() → success
Fairness & Ordering¶
redlock4j¶
Fairness: Non-fair (no ordering guarantees)
Characteristics: - Permits acquired in arbitrary order - No queue for waiting threads - Retry-based acquisition - First to successfully acquire wins
Example:
Thread 1: tryAcquire() → retry → retry → success
Thread 2: tryAcquire() → success (may acquire before Thread 1)
Thread 3: tryAcquire() → retry → timeout
Redisson¶
Fairness: Non-fair (explicitly documented)
Characteristics: - No FIFO ordering - Pub/sub wakes all waiters - Race to acquire after notification - First to execute Lua script wins
Example:
Thread 1: acquire() → blocks → subscribes
Thread 2: acquire() → blocks → subscribes
Thread 3: release() → PUBLISH
Thread 1 & 2: wake up → race to tryAcquire()
Winner: unpredictable
Note: Redisson also provides RedissonPermitExpirableSemaphore for fair semaphores with FIFO ordering.
Permit Counting & Availability¶
redlock4j¶
Counting: Implicit (count active permit keys)
public int availablePermits() {
// Note: This would require counting active permits across all nodes
// Current implementation returns maxPermits (placeholder)
return maxPermits;
}
Challenges: - No central counter - Would need to count keys matching pattern - Expensive operation (SCAN on all nodes) - Not implemented accurately
Actual Available Permits:
Redisson¶
Counting: Explicit counter
public int availablePermits() {
return get(availablePermitsAsync());
}
// Implementation
public RFuture<Integer> availablePermitsAsync() {
return commandExecutor.writeAsync(
getRawName(), LongCodec.INSTANCE,
RedisCommands.GET_INTEGER, getRawName()
);
}
Characteristics:
- Single GET operation
- Accurate and fast
- O(1) complexity
- Real-time availability
Initialization & Configuration¶
redlock4j¶
Initialization: Implicit (no setup required)
// Create and use immediately
RedlockSemaphore semaphore = manager.createSemaphore("api-limiter", 5);
// No need to set permits - maxPermits is just a limit
semaphore.tryAcquire();
Characteristics:
- maxPermits is a configuration parameter
- No Redis initialization needed
- Permits created on-demand
- No way to "drain" or "reset" permits
Redisson¶
Initialization: Explicit (must set permits)
RSemaphore semaphore = redisson.getSemaphore("api-limiter");
// Must initialize before use
semaphore.trySetPermits(5);
// Or add permits
semaphore.addPermits(5);
// Now can use
semaphore.tryAcquire();
Characteristics:
- Must explicitly set initial permits
- trySetPermits() - sets only if not exists
- addPermits() - adds to existing count
- drainPermits() - removes all permits
- Can reset/reconfigure at runtime
Additional Operations:
// Set permits with TTL
semaphore.trySetPermits(5, Duration.ofMinutes(10));
// Drain all permits
int drained = semaphore.drainPermits();
// Release even if not held (add permits)
semaphore.addPermits(3);
Timeout & Validity¶
redlock4j¶
Validity: Per-acquisition validity time
private static class PermitState {
final List<String> permitIds;
final Instant acquisitionTime;
final Duration validityDuration; // Calculated validity
boolean isValid() {
return Instant.now().isBefore(acquisitionTime.plus(validityDuration));
}
}
Characteristics: - Validity time calculated per acquisition - Clock drift compensation - Permits auto-expire via Redis TTL - Thread-local validity tracking
Validity Calculation:
// Delegated to the execution strategy, which applies clock-drift
// compensation to the configured lock timeout (a Duration).
Duration elapsed = Duration.between(startTime, Instant.now());
long validityTime = executionStrategy.calculateValidityTime(
config.getDefaultLockTimeout().toMillis(), elapsed.toMillis());
Redisson¶
Validity: No automatic expiration
Characteristics: - Permits don't expire automatically - Counter persists indefinitely - Can set TTL on semaphore key explicitly - No validity tracking per acquisition
Optional TTL:
// Set permits with expiration
semaphore.trySetPermits(5, Duration.ofMinutes(10));
// After 10 minutes, the entire semaphore key expires
// All permits lost
Performance Comparison¶
redlock4j¶
Acquisition (N permits on M nodes):
- N × M SET NX operations
- Sequential per permit
- Parallel across nodes
- Rollback on failure: N × M DELETE
Release (N permits on M nodes):
- N × M DELETE operations
- No notification overhead
Complexity: O(N × M) per operation
Latency: - Higher due to quorum requirement - Multiple round trips per permit - No pub/sub overhead
Example (3 permits, 3 nodes):
Acquire:
Permit 1: SET on Node1, Node2, Node3 (parallel)
Permit 2: SET on Node1, Node2, Node3 (parallel)
Permit 3: SET on Node1, Node2, Node3 (parallel)
Total: 9 operations
Release:
DELETE permit1 on Node1, Node2, Node3
DELETE permit2 on Node1, Node2, Node3
DELETE permit3 on Node1, Node2, Node3
Total: 9 operations
Redisson¶
Acquisition (N permits): - 1 Lua script execution (atomic) - If waiting: 1 pub/sub subscription - Notifications on release
Release (N permits):
- 1 Lua script execution
- 1 PUBLISH to channel
Complexity: O(1) per operation
Latency: - Lower for single instance - Single round trip - Pub/sub notification overhead
Example (3 permits):
Acquire:
1 Lua script: GET + DECRBY
If blocked: subscribe to channel
Total: 1-2 operations
Release:
1 Lua script: INCRBY + PUBLISH
Total: 1 operation
Measured Performance¶
Benchmark: 5 clients, 3 permits, 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 | 54.71 | 1.00 | 386.5 | 37.9 | 100 % |
| redlock4j-singlenode | 91.09 | 0.73 | 2.21 | 0.82 | 100 % |
| redlock4j-3node | 87.50 | 1.77 | 4.20 | 1.84 | 100 % |
Reading the numbers: redlock4j wins decisively on every metric — ~1.7× Redisson's throughput, ~175× lower p99, and ~46× lower mean latency. Redisson's pub/sub semaphore can stall waiters when the publish round-trips with the release; redlock4j's simpler per-permit SET NX approach turns out to be both faster and more predictable in the steady state. The 3-node variant pays only a ~4 % throughput tax for full quorum safety.
Safety & Correctness¶
redlock4j¶
Safety Guarantees: - Quorum-based consistency - Survives minority node failures - Clock drift compensation - Automatic permit expiration - No single point of failure
Potential Issues:
- Higher latency
- More network overhead
- availablePermits() not accurate
- No permit counting mechanism
- Polling-based (no notifications)
Consistency Model:
Permit acquired if:
- Quorum of nodes have the permit key
- Validity time > 0
- No clock drift issues
Redisson¶
Safety Guarantees: - Atomic operations (Lua scripts) - Accurate permit counting - Efficient pub/sub notifications - Async/reactive support - Low latency
Potential Issues: - Single point of failure (single instance) - No quorum mechanism - No automatic permit expiration - Permits persist indefinitely - Thundering herd on notification
Consistency Model:
Permit acquired if:
- Counter >= requested permits
- Atomic decrement succeeds
- No distributed consistency
Use Case Comparison¶
redlock4j RedlockSemaphore¶
Best For: - Distributed systems requiring quorum-based safety - Rate limiting with strong consistency - Scenarios where permit expiration is critical - Multi-master Redis setups - Fault-tolerant resource pooling
Example Scenarios:
// API rate limiting with fault tolerance
RedlockSemaphore apiLimiter = manager.createSemaphore("api:external:rate-limit", 100);
// Database connection pool with auto-expiration
RedlockSemaphore dbPool = manager.createSemaphore("db:connection:pool", 50);
// Distributed job throttling
RedlockSemaphore jobThrottle = manager.createSemaphore("jobs:concurrent-limit", 10);
Redisson RedissonSemaphore¶
Best For: - Single Redis instance deployments - High-throughput rate limiting - Scenarios requiring accurate permit counting - Applications needing async/reactive APIs - Dynamic permit management
Example Scenarios:
// High-throughput API rate limiting
RSemaphore apiLimiter = redisson.getSemaphore("api:rate-limit");
apiLimiter.trySetPermits(1000);
// Resource pool with dynamic sizing
RSemaphore resourcePool = redisson.getSemaphore("resource:pool");
resourcePool.addPermits(50); // Can adjust at runtime
// Async rate limiting
RSemaphore asyncLimiter = redisson.getSemaphore("async:limiter");
asyncLimiter.trySetPermits(100);
RFuture<Boolean> future = asyncLimiter.tryAcquireAsync(5, TimeUnit.SECONDS);
Complexity Analysis¶
redlock4j¶
Code Complexity: ~370 lines
Pros: - Quorum-based safety - Automatic permit expiration - Fault-tolerant - Clock drift compensation - Thread-local state tracking
Cons: - Higher latency - More Redis operations - No accurate permit counting - No permit management operations - Polling-based waiting
Redisson¶
Code Complexity: ~600 lines (with async support)
Pros: - Low latency - Atomic operations - Accurate permit counting - Pub/sub notifications - Async/reactive support - Rich API (drain, add, set permits)
Cons: - Single point of failure - No quorum mechanism - No automatic expiration - Permits persist indefinitely - More complex implementation
Feature Comparison Table¶
| Feature | redlock4j | Redisson |
|---|---|---|
| Data Model | Individual permit keys | Single counter |
| Quorum | Yes (per permit) | No |
| Fault Tolerance | Survives minority failures | Single point of failure |
| Permit Expiration | Automatic (TTL) | Manual (optional) |
| Permit Counting | Not accurate | Accurate (O(1)) |
| Waiting Mechanism | Polling | Pub/sub |
| Fairness | Non-fair | Non-fair |
| Async Support | No | Yes |
| Reactive Support | No | Yes |
| Initialization | Implicit | Explicit |
| Permit Management | Limited | Rich (add/drain/set) |
| Performance | O(N×M) | O(1) |
| Latency | Higher | Lower |
| Network Overhead | High | Low |
| Clock Drift | Compensated | Not applicable |
Recommendations¶
Choose redlock4j RedlockSemaphore when:¶
- Need quorum-based distributed consistency
- Require fault tolerance (multi-master)
- Automatic permit expiration is critical
- Can tolerate higher latency
- Prefer simpler initialization
Choose Redisson RedissonSemaphore when:¶
- Single Redis instance is acceptable
- Need high throughput / low latency
- Require accurate permit counting
- Need async/reactive APIs
- Want dynamic permit management
- Efficient waiting (pub/sub) is important
For Fair Semaphores:¶
- Use Redisson's
RedissonPermitExpirableSemaphorefor FIFO ordering - redlock4j doesn't currently provide fair semaphore
Migration Considerations¶
From Redisson to redlock4j¶
// Before (Redisson)
RSemaphore semaphore = redisson.getSemaphore("api-limiter");
semaphore.trySetPermits(5);
if (semaphore.tryAcquire(5, TimeUnit.SECONDS)) {
try {
// work
} finally {
semaphore.release();
}
}
// After (redlock4j)
RedlockSemaphore semaphore = manager.createSemaphore("api-limiter", 5);
if (semaphore.tryAcquire(Duration.ofSeconds(5))) {
try {
// work
} finally {
semaphore.release();
}
}
Benefits: - Quorum-based safety - Fault tolerance - Automatic expiration
Considerations: - Higher latency - No accurate permit counting - No dynamic permit management
From redlock4j to Redisson¶
// Before (redlock4j)
RedlockSemaphore semaphore = manager.createSemaphore("api-limiter", 5);
// After (Redisson)
RSemaphore semaphore = redisson.getSemaphore("api-limiter");
semaphore.trySetPermits(5);
Benefits: - Lower latency - Accurate permit counting - Async/reactive support - Dynamic permit management
Considerations: - Single point of failure - Must initialize explicitly - No automatic expiration
Conclusion¶
Both implementations serve different purposes:
redlock4j RedlockSemaphore: - Designed for distributed consistency with quorum-based safety - Individual permit keys with automatic expiration - Higher latency but fault-tolerant - Simpler initialization, limited management - Best for multi-master setups requiring strong consistency
Redisson RedissonSemaphore: - Designed for high-performance single-instance deployments - Atomic counter with pub/sub notifications - Lower latency but single point of failure - Rich API with dynamic permit management - Best for high-throughput scenarios with single Redis instance
Choose based on your specific requirements: - Distributed consistency & fault tolerance → redlock4j RedlockSemaphore - High throughput & low latency → Redisson RedissonSemaphore - Fair ordering (FIFO) → Redisson RedissonPermitExpirableSemaphore