MultiLock Implementation Comparison: redlock4j vs Redisson¶
This document provides a detailed technical comparison of the MultiLock implementations in redlock4j and Redisson.
Overview¶
Both libraries implement multi-lock functionality to atomically acquire multiple locks, but they use fundamentally different approaches and have different design goals.
Purpose & Use Case¶
redlock4j MultiLock¶
Purpose: Atomic acquisition of multiple independent resources with deadlock prevention
Use Case: When you need to lock multiple resources simultaneously (e.g., transferring between multiple bank accounts)
Lock multiLock = manager.createMultiLock(
Arrays.asList("account:1", "account:2", "account:3")
);
multiLock.lock();
try {
// All three accounts are now locked
transferBetweenAccounts();
} finally {
multiLock.unlock();
}
Redisson RedissonMultiLock¶
Purpose: Group multiple RLock objects and manage them as a single lock
Use Case: When you have multiple independent locks (possibly from different Redisson instances) that need to be acquired together
RLock lock1 = redisson1.getLock("lock1");
RLock lock2 = redisson2.getLock("lock2");
RLock lock3 = redisson3.getLock("lock3");
RedissonMultiLock multiLock = new RedissonMultiLock(lock1, lock2, lock3);
multiLock.lock();
try {
// All locks acquired
} finally {
multiLock.unlock();
}
Architecture & Design Philosophy¶
redlock4j¶
Design: Integrated Redlock implementation for multiple resources
Key Characteristics: - Each resource is a separate key on the same Redis cluster - Uses the same Redlock quorum mechanism for all resources - Sorted keys to prevent deadlocks - All-or-nothing acquisition with automatic rollback
Architecture:
MultiLock
├─ List<String> lockKeys (sorted)
├─ LockExecutionStrategy (node fan-out + quorum)
├─ Quorum-based acquisition per resource
└─ Thread-local state tracking
Redisson¶
Design: Wrapper around multiple independent RLock objects
Key Characteristics: - Each RLock can be from a different Redisson instance - Each lock uses its own Redis connection/cluster - No inherent deadlock prevention (no key sorting) - Sequential acquisition with retry logic
Architecture:
RedissonMultiLock
├─ List<RLock> locks (order preserved)
├─ Each RLock has its own connection
├─ Sequential acquisition
└─ Configurable failure tolerance
Deadlock Prevention¶
redlock4j¶
Strategy: Automatic key sorting
// Constructor automatically sorts keys
this.lockKeys = lockKeys.stream()
.distinct()
.sorted() // Lexicographic ordering
.collect(Collectors.toList());
Guarantee: All threads acquire locks in the same order, preventing circular wait conditions
Example:
// Thread 1: locks ["account:1", "account:2", "account:3"]
// Thread 2: locks ["account:3", "account:1", "account:2"]
// Both will acquire in order: account:1 → account:2 → account:3
Redisson¶
Strategy: No automatic deadlock prevention
// Locks are acquired in the order provided
public RedissonMultiLock(RLock... locks) {
this.locks.addAll(Arrays.asList(locks));
}
Risk: Developer must ensure consistent ordering
Example:
// Thread 1: new RedissonMultiLock(lock1, lock2, lock3)
// Thread 2: new RedissonMultiLock(lock3, lock1, lock2)
// DEADLOCK POSSIBLE if not careful!
Lock Acquisition Algorithm¶
redlock4j¶
Algorithm: Quorum-based atomic acquisition
private MultiLockResult attemptMultiLock() {
Map<String, String> lockValues = new HashMap<>();
// 1. Generate unique values for each key
for (String key : lockKeys) {
lockValues.put(key, generateLockValue());
}
// 2. Try to acquire ALL locks on EACH Redis node.
// The execution strategy fans out to the nodes and returns how many
// succeeded.
int successfulNodes = executionStrategy.executeOnNodes(
driver -> acquireAllOnNode(driver, lockValues));
// 3. Check quorum (via the strategy) and validity
long validityTime = executionStrategy.calculateValidityTime(
config.getDefaultLockTimeout().toMillis(), startTime);
boolean acquired = executionStrategy.isSuccessful(successfulNodes)
&& validityTime > 0;
// 4. Rollback if failed
if (!acquired) {
releaseAllLocks(lockValues);
}
return new MultiLockResult(acquired, validityTime, lockValues, ...);
}
Per-Node Acquisition:
private boolean acquireAllOnNode(RedisDriver driver, Map<String, String> lockValues) {
List<String> acquiredKeys = new ArrayList<>();
for (String key : lockKeys) {
if (driver.setIfNotExists(key, lockValue, timeout)) {
acquiredKeys.add(key);
} else {
// Failed - rollback this node
rollbackOnNode(driver, lockValues, acquiredKeys);
return false;
}
}
return true;
}
Flow: 1. Generate unique lock values for all keys 2. For each Redis node: - Try to acquire ALL locks - If any fails, rollback that node 3. Check if quorum achieved 4. If not, release all acquired locks
Redisson¶
Algorithm: Sequential acquisition with retry and failure tolerance
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) {
long remainTime = unit.toMillis(waitTime);
long lockWaitTime = calcLockWaitTime(remainTime);
int failedLocksLimit = failedLocksLimit(); // Default: 0
List<RLock> acquiredLocks = new ArrayList<>();
// 1. Iterate through locks sequentially
for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) {
RLock lock = iterator.next();
boolean lockAcquired;
try {
// 2. Try to acquire this lock
long awaitTime = Math.min(lockWaitTime, remainTime);
lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
} catch (Exception e) {
lockAcquired = false;
}
if (lockAcquired) {
acquiredLocks.add(lock);
} else {
// 3. Check if we can tolerate this failure
if (locks.size() - acquiredLocks.size() == failedLocksLimit()) {
break; // Acquired enough
}
// 4. If no tolerance, retry from beginning
if (failedLocksLimit == 0) {
unlockInner(acquiredLocks);
if (waitTime <= 0) {
return false;
}
// Reset and retry
acquiredLocks.clear();
while (iterator.hasPrevious()) {
iterator.previous();
}
} else {
failedLocksLimit--;
}
}
// 5. Check timeout
if (remainTime > 0) {
remainTime -= elapsed;
if (remainTime <= 0) {
unlockInner(acquiredLocks);
return false;
}
}
}
// 6. Set lease time on all acquired locks
if (leaseTime > 0) {
acquiredLocks.stream()
.map(l -> (RedissonBaseLock) l)
.map(l -> l.expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS))
.forEach(f -> f.toCompletableFuture().join());
}
return true;
}
Flow: 1. Iterate through locks in provided order 2. Try to acquire each lock individually 3. If failure and no tolerance: unlock all and retry from start 4. If failure with tolerance: continue to next lock 5. Check timeout after each attempt 6. Set lease time on all acquired locks
Failure Handling & Rollback¶
redlock4j¶
Strategy: Per-node rollback with all-or-nothing semantics
private void rollbackOnNode(RedisDriver driver, Map<String, String> lockValues,
List<String> acquiredKeys) {
for (String key : acquiredKeys) {
try {
driver.deleteIfValueMatches(key, lockValues.get(key));
} catch (Exception e) {
logger.warn("Failed to rollback lock {} on {}", key, driver);
}
}
}
Characteristics: - Immediate rollback on any failure within a node - All-or-nothing per node - Quorum check after all nodes attempted - Global rollback if quorum not achieved
Example:
Node 1: account:1 ✓, account:2 ✓, account:3 ✓ → Success
Node 2: account:1 ✓, account:2 ✗ → Rollback account:1 on Node 2
Node 3: account:1 ✓, account:2 ✓, account:3 ✓ → Success
Result: 2/3 nodes succeeded
If quorum=2: SUCCESS
If quorum=3: FAIL → Rollback all nodes
Redisson¶
Strategy: Retry from beginning on failure (default) or tolerance-based
Characteristics: - Default: unlock all and retry from start - Can be overridden for failure tolerance - No per-lock rollback - Sequential retry logic
Example (default behavior):
Example (with tolerance in RedissonRedLock):
RedissonRedLock extends RedissonMultiLock {
protected int failedLocksLimit() {
return locks.size() - minLocksAmount(locks);
}
protected int minLocksAmount(List<RLock> locks) {
return locks.size() / 2 + 1; // Quorum
}
}
Attempt: lock1 ✓, lock2 ✗, lock3 ✓ → Success (2/3 acquired)
Reentrancy Support¶
redlock4j¶
Implementation: Thread-local state with hold count
private final ThreadLocal<LockState> lockState = new ThreadLocal<>();
private static class LockState {
final Map<String, String> lockValues; // All lock values
final Instant acquisitionTime;
final Duration validityDuration;
int holdCount;
}
public boolean tryLock(long time, TimeUnit unit) {
// Check reentrancy
LockState currentState = lockState.get();
if (currentState != null && currentState.isValid()) {
currentState.incrementHoldCount();
return true;
}
// ... acquire logic
}
public void unlock() {
LockState state = lockState.get();
int remainingHolds = state.decrementHoldCount();
if (remainingHolds > 0) {
return; // Still held
}
// ... release logic
}
Characteristics: - Single hold count for all locks together - Fast (no Redis calls for reentrant acquisition) - Thread-local storage - All locks treated as atomic unit
Redisson¶
Implementation: Delegates to individual RLock reentrancy
// Each RLock handles its own reentrancy
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) {
for (RLock lock : locks) {
// Each lock checks its own hold count in Redis
lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
}
}
public void unlock() {
locks.forEach(Lock::unlock);
}
Characteristics: - Each lock maintains its own hold count - Requires Redis calls for each lock - Independent reentrancy per lock - Locks can have different hold counts
Timeout & Validity Calculation¶
redlock4j¶
Strategy: Single validity time for all locks
long startTime = System.currentTimeMillis();
// Acquire all locks...
// The execution strategy computes the remaining validity, applying clock-drift
// compensation to the configured lock timeout (a Duration).
long validityTime = executionStrategy.calculateValidityTime(
config.getDefaultLockTimeout().toMillis(), startTime);
boolean acquired = executionStrategy.isSuccessful(successfulNodes) && validityTime > 0;
Characteristics: - Single validity calculation for entire multi-lock - Clock drift compensation - All locks expire together - Validity must be positive for success
Redisson¶
Strategy: Individual timeout per lock with dynamic wait time
long baseWaitTime = locks.size() * 1500; // 1.5s per lock
long waitTime;
if (leaseTime <= 0) {
waitTime = baseWaitTime;
} else {
waitTime = unit.toMillis(leaseTime);
if (waitTime <= baseWaitTime) {
waitTime = ThreadLocalRandom.current().nextLong(waitTime/2, waitTime);
} else {
waitTime = ThreadLocalRandom.current().nextLong(baseWaitTime, waitTime);
}
}
Characteristics: - Dynamic wait time based on number of locks - Randomization to avoid thundering herd - Each lock can have different lease time - No global validity check
Performance Comparison¶
redlock4j¶
Lock Acquisition:
- For N resources on M nodes:
- N × M SET NX operations (parallel per node)
- Rollback: up to N × M DELETE operations
- Single round of attempts per retry
- All nodes contacted in parallel
Complexity: O(N × M) per attempt
Example (3 resources, 3 nodes):
Attempt 1:
Node 1: SET account:1, SET account:2, SET account:3
Node 2: SET account:1, SET account:2, SET account:3
Node 3: SET account:1, SET account:2, SET account:3
Total: 9 operations (parallel)
Redisson¶
Lock Acquisition: - For N locks: - N sequential lock attempts - Each lock may involve multiple Redis operations - Retry from beginning on failure - Sequential processing
Complexity: O(N × R) where R = retry attempts
Example (3 locks):
Attempt 1:
lock1.tryLock() → Redis operations
lock2.tryLock() → Redis operations
lock2 fails → unlock lock1
Attempt 2:
lock1.tryLock() → Redis operations
lock2.tryLock() → Redis operations
lock3.tryLock() → Redis operations
Total: Variable, sequential
Measured Performance¶
Benchmark: 5 clients, 5 resources per MultiLock, 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 | 17.05 | 190.2 | 1,192.2 | 272.9 | 100 % |
| redlock4j-singlenode | 16.97 | 202.9 | 1,709.5 | 303.0 | 100 % |
| redlock4j-3node | 17.72 | 171.0 | 1,057.6 | 237.4 | 100 % |
Reading the numbers: redlock4j-3node leads on throughput, p50, p99, and mean — the only primitive where the 3-node Redlock variant beats every single-node implementation in the field. The parallel multi-node I/O (each SET NX for all 5 resources fans out across all 3 nodes simultaneously) is what makes this possible.
Use Case Differences¶
redlock4j MultiLock¶
Best For: - Locking multiple resources on the same Redis cluster - Scenarios requiring strict deadlock prevention - Atomic operations across multiple keys - Distributed systems with quorum requirements
Example Scenarios:
// Bank transfer between multiple accounts
Lock lock = manager.createMultiLock(
Arrays.asList("account:1", "account:2", "account:3")
);
// Inventory management across warehouses
Lock lock = manager.createMultiLock(
Arrays.asList("warehouse:A:item:123", "warehouse:B:item:123")
);
Redisson RedissonMultiLock¶
Best For: - Grouping locks from different Redis instances - Coordinating across multiple independent systems - Flexible lock composition - When you already have RLock objects
Example Scenarios:
// Locks from different Redis clusters
RLock lock1 = redisson1.getLock("resource1"); // Cluster 1
RLock lock2 = redisson2.getLock("resource2"); // Cluster 2
RLock lock3 = redisson3.getLock("resource3"); // Cluster 3
RedissonMultiLock multiLock = new RedissonMultiLock(lock1, lock2, lock3);
// Mix different lock types
RLock fairLock = redisson.getFairLock("fair");
RLock readLock = redisson.getReadWriteLock("rw").readLock();
RedissonMultiLock multiLock = new RedissonMultiLock(fairLock, readLock);
Safety & Correctness¶
redlock4j¶
Safety Guarantees: - Deadlock-free (automatic key sorting) - Quorum-based consistency - All-or-nothing atomicity - Clock drift compensation - Validity time enforcement
Potential Issues: - All resources must be on same Redis cluster - Higher latency due to quorum requirement - More network overhead (N×M operations)
Redisson¶
Safety Guarantees: - Flexible lock composition - Works across different Redis instances - Extensible failure tolerance - Async/reactive support
Potential Issues: - No automatic deadlock prevention - Developer must ensure lock ordering - Sequential acquisition (slower for many locks) - No quorum mechanism by default
Complexity Analysis¶
redlock4j¶
Code Complexity: ~370 lines
Pros: - Integrated Redlock implementation - Automatic deadlock prevention - Clear all-or-nothing semantics - Single validity time - Thread-local state (fast reentrancy)
Cons: - Limited to single Redis cluster - More Redis operations - Higher network overhead - Less flexible composition
Redisson¶
Code Complexity: ~450 lines (with async support)
Pros: - Works across multiple Redis instances - Flexible lock composition - Extensible (can override failedLocksLimit) - Async/reactive support - Can mix different lock types
Cons: - No deadlock prevention - Sequential acquisition - More complex retry logic - Requires careful lock ordering
RedissonRedLock vs redlock4j MultiLock¶
Redisson also has RedissonRedLock which extends RedissonMultiLock:
RedissonRedLock¶
public class RedissonRedLock extends RedissonMultiLock {
@Override
protected int failedLocksLimit() {
return locks.size() - minLocksAmount(locks);
}
protected int minLocksAmount(List<RLock> locks) {
return locks.size() / 2 + 1; // Quorum
}
}
Key Difference: Implements quorum-based failure tolerance
Comparison with redlock4j MultiLock:
| Feature | redlock4j MultiLock | RedissonRedLock |
|---|---|---|
| Purpose | Multiple resources on same cluster | Multiple independent Redis instances |
| Quorum | Per-resource across nodes | Across different locks |
| Deadlock Prevention | Automatic (sorted keys) | Manual (developer responsibility) |
| Acquisition | Parallel per node | Sequential across locks |
| Use Case | Multi-resource locking | Multi-instance Redlock |
Recommendations¶
Choose redlock4j MultiLock when:¶
- Locking multiple resources on the same Redis cluster
- Need automatic deadlock prevention
- Require strict all-or-nothing semantics
- Want quorum-based safety per resource
- Prefer simpler, integrated solution
Choose Redisson RedissonMultiLock when:¶
- Need to coordinate locks across different Redis instances
- Want to compose different lock types
- Require async/reactive support
- Can manage lock ordering manually
- Need flexible failure tolerance
Choose Redisson RedissonRedLock when:¶
- Implementing Redlock across multiple Redis instances
- Each lock represents a different Redis master
- Need quorum-based distributed locking
- Can ensure proper lock ordering
Migration Considerations¶
From Redisson to redlock4j¶
// Before (Redisson)
RLock lock1 = redisson.getLock("account:1");
RLock lock2 = redisson.getLock("account:2");
RLock lock3 = redisson.getLock("account:3");
RedissonMultiLock multiLock = new RedissonMultiLock(lock1, lock2, lock3);
multiLock.lock();
try {
// work
} finally {
multiLock.unlock();
}
// After (redlock4j)
Lock multiLock = manager.createMultiLock(
Arrays.asList("account:1", "account:2", "account:3")
);
multiLock.lock();
try {
// work
} finally {
multiLock.unlock();
}
Benefits: - Automatic deadlock prevention - Quorum-based safety - Simpler API
Considerations: - All resources must be on same cluster - Different performance characteristics
From redlock4j to Redisson¶
// Before (redlock4j)
Lock multiLock = manager.createMultiLock(
Arrays.asList("resource1", "resource2", "resource3")
);
// After (Redisson) - if using multiple instances
RLock lock1 = redisson1.getLock("resource1");
RLock lock2 = redisson2.getLock("resource2");
RLock lock3 = redisson3.getLock("resource3");
RedissonRedLock multiLock = new RedissonRedLock(lock1, lock2, lock3);
Benefits: - Can use different Redis instances - Async/reactive support - More flexible composition
Considerations: - Must ensure consistent lock ordering - Different acquisition semantics
Conclusion¶
Both implementations serve different purposes:
redlock4j MultiLock: - Designed for locking multiple resources on the same distributed Redis cluster - Automatic deadlock prevention through key sorting - Quorum-based safety per resource - All-or-nothing atomic acquisition - Simpler, more focused implementation
Redisson RedissonMultiLock: - Designed for grouping independent locks (possibly from different instances) - Flexible composition of different lock types - Sequential acquisition with retry logic - Requires manual deadlock prevention - More flexible but more complex
Redisson RedissonRedLock: - Implements Redlock algorithm across multiple Redis instances - Quorum-based failure tolerance - Similar goals to redlock4j but different scope (instances vs resources)
Choose based on your specific requirements: - Same cluster, multiple resources → redlock4j MultiLock - Multiple instances, flexible composition → Redisson RedissonMultiLock - Multiple instances, Redlock algorithm → Redisson RedissonRedLock