Skip to content

ReadWriteLock Implementation Comparison: redlock4j vs Redisson

This document provides a detailed technical comparison of the ReadWriteLock implementations in redlock4j and Redisson.

Overview

Both libraries implement distributed read-write locks to allow multiple concurrent readers or a single exclusive writer, but they use different data structures and synchronization mechanisms.

Purpose & Use Case

redlock4j RedlockReadWriteLock

Purpose: Distributed read-write lock with quorum-based safety guarantees

Use Case: Scenarios requiring strong consistency for read-heavy workloads

RedlockReadWriteLock rwLock = manager.createReadWriteLock("resource");

// Multiple readers can acquire simultaneously
rwLock.readLock().lock();
try {
    readData();
} finally {
    rwLock.readLock().unlock();
}

// Writer has exclusive access
rwLock.writeLock().lock();
try {
    writeData();
} finally {
    rwLock.writeLock().unlock();
}

Redisson RedissonReadWriteLock

Purpose: Distributed read-write lock with pub/sub notifications

Use Case: High-performance read-write scenarios with single Redis instance

RReadWriteLock rwLock = redisson.getReadWriteLock("resource");

// Read lock
rwLock.readLock().lock();
try {
    readData();
} finally {
    rwLock.readLock().unlock();
}

// Write lock
rwLock.writeLock().lock();
try {
    writeData();
} finally {
    rwLock.writeLock().unlock();
}

Architecture & Data Model

redlock4j

Design: Counter-based with separate write lock

Data Structure:

{resourceKey}:readers = {count}                    (reader counter)
{resourceKey}:readers:{lockValue1} = "1"           (individual reader tracking)
{resourceKey}:readers:{lockValue2} = "1"           (individual reader tracking)
{resourceKey}:write = {lockValue}                  (exclusive write lock)

Key Characteristics: - Reader count tracked via INCR/DECR - Individual reader keys for tracking - Write lock uses standard Redlock - Quorum-based for both read and write - Thread-local state for reentrancy

Architecture:

RedlockReadWriteLock
  ├─ ReadLock
  │   ├─ readCountKey ({key}:readers)
  │   ├─ writeLockKey ({key}:write)
  │   ├─ ThreadLocal<LockState>
  │   └─ Quorum-based INCR/DECR
  └─ WriteLock
      ├─ Redlock (for write lock)
      ├─ readCountKey ({key}:readers)
      └─ Polling for reader count

Redisson

Design: Hash-based with mode tracking

Data Structure:

{resourceKey} = {
  "mode": "read" or "write",
  "{threadId1}": {holdCount},
  "{threadId2}": {holdCount},
  ...
}
redisson_rwlock:{resourceKey} = (pub/sub channel)

Key Characteristics: - Single hash stores all lock state - Mode field tracks read/write state - Thread IDs as hash fields - Lua scripts for atomicity - Pub/sub for notifications

Architecture:

RedissonReadWriteLock
  ├─ RedissonReadLock
  │   ├─ Lua scripts for acquisition
  │   ├─ Hash-based state
  │   └─ Pub/sub notifications
  └─ RedissonWriteLock
      ├─ Lua scripts for acquisition
      ├─ Hash-based state
      └─ Pub/sub notifications

Read Lock Acquisition

redlock4j

Algorithm: Check write lock, then increment reader count

public boolean tryLock(Duration timeout) throws InterruptedException {
    // 1. Check reentrancy (thread-local state)
    LockState currentState = lockState.get();
    if (currentState != null && currentState.isValid()) {
        currentState.incrementHoldCount();
        return true;
    }

    Instant deadline = Instant.now().plus(timeout);

    // 2. Retry loop
    for (int attempt = 0; attempt <= config.getMaxRetryAttempts(); attempt++) {
        // 3. Only proceed when no writer holds the lock
        if (!isWriteLockHeld()) {
            String lockValue = generateLockValue();
            // 4. Increment the reader count (quorum-based via the strategy)
            if (incrementReaderCount(lockValue)) {
                lockState.set(new LockState(lockValue, Instant.now(),
                                            config.getDefaultLockTimeout()));
                return true;
            }
        }

        // 5. Stop once the timeout is exhausted, otherwise wait and retry
        Duration remaining = Duration.between(Instant.now(), deadline);
        if (!timeout.isZero() && remaining.isNegative()) {
            break;
        }
        waitForLockRelease(remaining.toMillis(), attempt);
    }

    return false;
}

Flow: 1. Check thread-local reentrancy 2. Poll until no writer holds the lock 3. Increment the reader count (quorum-based) 4. Track validity in thread-local state

Redisson

Algorithm: Lua script with hash-based state management

-- tryLockInnerAsync (simplified)
local mode = redis.call('hget', KEYS[1], 'mode');

-- If no lock or already in read mode
if (mode == false) then
    redis.call('hset', KEYS[1], 'mode', 'read');
    redis.call('hset', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;

if (mode == 'read') or (mode == 'write' and redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    local ind = redis.call('hincrby', KEYS[1], ARGV[2], 1);
    local remainTime = redis.call('pttl', KEYS[1]);
    redis.call('pexpire', KEYS[1], math.max(remainTime, ARGV[1]));
    return nil;
end;

-- Write lock held by another thread
return redis.call('pttl', KEYS[1]);

Flow: 1. Get current mode from hash 2. If no lock: set mode='read', add thread, set TTL 3. If read mode: increment thread's hold count 4. If write mode by same thread: allow (lock downgrade) 5. If write mode by other thread: return TTL

Redis Operations: - 1 Lua script execution (atomic)

Write Lock Acquisition

redlock4j

Algorithm: Wait for readers, then acquire exclusive lock

public boolean tryLock(long time, TimeUnit unit) {
    long timeoutMs = unit.toMillis(time);
    long startTime = System.currentTimeMillis();

    // 1. Wait for readers to finish
    while (hasActiveReaders()) {
        if (timeoutExceeded(startTime, timeoutMs)) {
            return false;
        }
        Thread.sleep(retryDelayMs);
    }

    // 2. Acquire write lock using standard Redlock
    long remainingTime = timeoutMs - elapsed;
    return underlyingLock.tryLock(remainingTime, TimeUnit.MILLISECONDS);
}

private boolean hasActiveReaders() {
    // The execution strategy fans out to the nodes; a node "votes" when it
    // reports no active readers.
    int nodesWithoutReaders = executionStrategy.executeOnNodes(driver -> {
        String countStr = driver.get(readCountKey);
        return countStr == null || Long.parseLong(countStr) <= 0;
    });

    // Readers are considered gone once a quorum of nodes report none
    return !executionStrategy.isSuccessful(nodesWithoutReaders);
}

Flow: 1. Poll reader count on all nodes 2. Wait until quorum shows no readers 3. Acquire exclusive lock via Redlock 4. Check timeout throughout

Redis Operations (M nodes): - N × M GET (polling reader count) - M × SET NX (Redlock acquisition)

Redisson

Algorithm: Lua script with mode transition

-- tryLockInnerAsync (simplified)
local mode = redis.call('hget', KEYS[1], 'mode');

-- No lock exists
if (mode == false) then
    redis.call('hset', KEYS[1], 'mode', 'write');
    redis.call('hset', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;

-- Same thread already holds write lock (reentrant)
if (mode == 'write') then
    if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
        redis.call('hincrby', KEYS[1], ARGV[2], 1);
        redis.call('pexpire', KEYS[1], ARGV[1]);
        return nil;
    end;
end;

-- Same thread holds read lock (lock upgrade)
if (mode == 'read') then
    local ind = redis.call('hget', KEYS[1], ARGV[2]);
    if (ind ~= false) then
        -- Check if this is the only reader
        if (redis.call('hlen', KEYS[1]) == 2) then -- mode + this thread
            redis.call('hset', KEYS[1], 'mode', 'write');
            return nil;
        end;
    end;
end;

-- Lock held by others
return redis.call('pttl', KEYS[1]);

Flow: 1. Get current mode 2. If no lock: set mode='write', add thread 3. If write mode by same thread: increment (reentrant) 4. If read mode by same thread only: upgrade to write 5. If held by others: return TTL

Redis Operations: - 1 Lua script execution (atomic)

Read Lock Release

redlock4j

Algorithm: Decrement reader count

public void unlock() {
    LockState state = lockState.get();

    // Handle reentrancy
    int remainingHolds = state.decrementHoldCount();
    if (remainingHolds > 0) {
        return;
    }

    // Decrement reader count on all nodes
    decrementReaderCount(state.lockValue);
    lockState.remove();
}

private void decrementReaderCount(String lockValue) {
    // Fan out to the appropriate nodes via the execution strategy
    executionStrategy.executeOnNodes(driver -> {
        // Decrement counter
        long count = driver.decr(readCountKey);

        // Delete individual reader key
        driver.del(readCountKey + ":" + lockValue);

        // Clean up counter if zero
        if (count <= 0) {
            driver.del(readCountKey);
        }
        return true;
    });
}

Characteristics: - DECR on all nodes - Delete individual reader key - Clean up counter when zero - No notification to waiting writers

Redis Operations (M nodes): - M × DECR - M × DEL (individual key) - M × DEL (counter, if zero)

Redisson

Algorithm: Lua script with notification

-- unlockInnerAsync (simplified)
local mode = redis.call('hget', KEYS[1], 'mode');
if (mode == false) then
    return 1; -- Already unlocked
end;

local lockExists = redis.call('hexists', KEYS[1], ARGV[2]);
if (lockExists == 0) then
    return nil; -- Not held by this thread
end;

local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1);
if (counter > 0) then
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return 0; -- Still held (reentrant)
end;

redis.call('hdel', KEYS[1], ARGV[2]);
if (redis.call('hlen', KEYS[1]) > 1) then
    -- Other readers still exist
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return 0;
end;

-- Last reader, delete lock and notify
redis.call('del', KEYS[1]);
redis.call('publish', KEYS[2], ARGV[1]);
return 1;

Characteristics: - Atomic decrement in hash - Delete thread field when zero - Publish notification when last reader - Waiting writers wake up immediately

Redis Operations: - 1 Lua script execution - 1 PUBLISH (if last reader)

Write Lock Release

redlock4j

Algorithm: Standard Redlock unlock

public void unlock() {
    underlyingLock.unlock(); // Delegates to Redlock
}

Characteristics: - Uses Redlock unlock mechanism - Delete if value matches - No notification to waiting readers/writers

Redis Operations (M nodes): - M × Lua script (delete if matches)

Redisson

Algorithm: Lua script with notification

-- Similar to read unlock but for write mode
-- Decrements hold count, deletes when zero, publishes notification

Characteristics: - Atomic decrement in hash - Delete lock when zero - Publish notification to all waiters - Immediate wake-up

Redis Operations: - 1 Lua script execution - 1 PUBLISH

Lock Upgrade/Downgrade

redlock4j

Lock Upgrade (Read → Write): Not supported

Lock Downgrade (Write → Read): Not supported

Characteristics: - Must release read lock before acquiring write lock - Must release write lock before acquiring read lock - No automatic conversion - Prevents potential deadlocks

Redisson

Lock Upgrade (Read → Write): Supported (single reader only)

-- If this thread is the only reader, can upgrade to write
if (mode == 'read') then
    if (redis.call('hlen', KEYS[1]) == 2) then -- mode + this thread
        redis.call('hset', KEYS[1], 'mode', 'write');
        return nil;
    end;
end;

Lock Downgrade (Write → Read): Supported

-- If thread holds write lock, can acquire read lock (downgrade)
if (mode == 'write' and redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    local ind = redis.call('hincrby', KEYS[1], ARGV[2], 1);
    return nil;
end;

Characteristics: - Automatic lock downgrade (write → read) - Lock upgrade only if sole reader - Prevents deadlock from multiple readers upgrading - More flexible but more complex

Fairness & Ordering

redlock4j

Fairness: Non-fair (no ordering guarantees)

Characteristics: - Readers and writers compete equally - No FIFO queue - Retry-based acquisition - Potential writer starvation if many readers

Example:

Reader 1: acquire → success
Reader 2: acquire → success
Writer 1: tryLock → blocks (waiting for readers)
Reader 3: acquire → success (can acquire while writer waits)
Writer 1: still waiting...

Redisson

Fairness: Non-fair by default

Characteristics: - No ordering between readers and writers - Pub/sub wakes all waiters - Race to acquire after notification - Also has RedissonFairReadWriteLock for FIFO ordering

Note: Redisson provides RedissonFairReadWriteLock for fair ordering with FIFO guarantees.

Performance Comparison

redlock4j

Read Lock Acquisition (M nodes): - M × GET (check write lock) - M × INCR (increment counter) - M × SETEX (individual reader key) - Total: 3M operations

Write Lock Acquisition (M nodes): - N × M GET (poll reader count) - M × SET NX (Redlock) - Total: (N+1)M operations

Complexity: O(M) for reads, O(N×M) for writes

Latency: - Higher due to quorum requirement - Polling overhead for writers - No pub/sub overhead

Redisson

Read Lock Acquisition: - 1 Lua script execution - If blocked: 1 pub/sub subscription - Total: 1-2 operations

Write Lock Acquisition: - 1 Lua script execution - If blocked: 1 pub/sub subscription - Total: 1-2 operations

Complexity: O(1) per operation

Latency: - Lower for single instance - Single round trip - Pub/sub notification overhead

Measured Performance

Benchmark: 10 clients (8 readers + 2 writers), 50 ms work per cycle, 3-node Redis 7 cluster, 60 s measurement (full methodology in Architecture › Performance Analysis).

Readers:

Implementation Ops/s p50 (ms) p99 (ms) mean (ms) success
Redisson reader 151.65 0.79 5.82 1.32 100 %
redlock4j-3node reader 95.41 2.72 348.4 30.4 100 %
redlock4j-singlenode reader 74.66 12.54 340.6 54.2 100 %

Writers:

Implementation Ops/s p50 (ms) p99 (ms) mean (ms) success
Redisson writer 0.21 8,649 16,821 9,680 100 %
redlock4j-singlenode writer 15.63 57.8 755.6 72.4 100 %
redlock4j-3node writer 16.42 64.1 104.2 63.5 100 %

Reading the numbers:

  • Readers: Redisson wins decisively. Its reader implementation tracks active readers via an in-process semaphore counter and only hits Redis on the transition; redlock4j hits Redis on every read acquire. This is an architectural choice, not a regression — the trade-off is that Redisson's reader count is local to one JVM while redlock4j's is distributed.
  • Writers: redlock4j wins decisively on both throughput and tail latency. Redisson exhibits writer starvation under reader load (p99 = 16.8 s); redlock4j's 3-node mode keeps writer p99 under 105 ms thanks to the parallel multi-node acquire path.

Safety & Correctness

redlock4j

Safety Guarantees: - Quorum-based consistency - Survives minority node failures - Multiple readers guaranteed - Exclusive writer guaranteed - No single point of failure

Potential Issues: - Higher latency - More network overhead - Polling-based (no notifications) - Potential writer starvation - No lock upgrade/downgrade

Consistency Model:

Read lock acquired if:
  - Quorum shows no write lock
  - Reader count incremented on quorum

Write lock acquired if:
  - Quorum shows no readers
  - Exclusive lock acquired on quorum

Redisson

Safety Guarantees: - Atomic operations (Lua scripts) - Multiple readers guaranteed - Exclusive writer guaranteed - Lock upgrade/downgrade support - Pub/sub notifications - Async/reactive support

Potential Issues: - Single point of failure - No quorum mechanism - Potential writer starvation (non-fair) - More complex Lua scripts

Consistency Model:

Lock acquired if:
  - Mode allows acquisition
  - Atomic state transition succeeds
  - No distributed consistency

Use Case Comparison

redlock4j RedlockReadWriteLock

Best For: - Distributed systems requiring quorum-based safety - Read-heavy workloads with strong consistency - Multi-master Redis setups - Fault-tolerant read-write scenarios - Can tolerate higher latency

Example Scenarios:

// Distributed cache with strong consistency
RedlockReadWriteLock cacheLock = manager.createReadWriteLock("cache:users");

// Configuration management
RedlockReadWriteLock configLock = manager.createReadWriteLock("config:app");

Redisson RedissonReadWriteLock

Best For: - Single Redis instance deployments - High-throughput read-write scenarios - Applications needing async/reactive APIs - Lock upgrade/downgrade requirements - Low-latency requirements

Example Scenarios:

// High-performance cache
RReadWriteLock cacheLock = redisson.getReadWriteLock("cache:users");

// Document editing with lock downgrade
RReadWriteLock docLock = redisson.getReadWriteLock("doc:123");
docLock.writeLock().lock();
try {
    editDocument();
    docLock.readLock().lock(); // Downgrade
    docLock.writeLock().unlock();
    try {
        readDocument();
    } finally {
        docLock.readLock().unlock();
    }
} finally {
    if (docLock.writeLock().isHeldByCurrentThread()) {
        docLock.writeLock().unlock();
    }
}

Feature Comparison Table

Feature redlock4j Redisson
Data Model Counter + individual keys Hash with mode field
Quorum Yes No
Fault Tolerance Survives minority failures Single point of failure
Lock Upgrade No Yes (single reader only)
Lock Downgrade No Yes
Waiting Mechanism Polling Pub/sub
Fairness Non-fair Non-fair (fair variant available)
Async Support No Yes
Reactive Support No Yes
Performance O(M) reads, O(N×M) writes O(1)
Latency Higher Lower
Network Overhead High Low
Atomicity Quorum-based Lua scripts

Recommendations

Choose redlock4j RedlockReadWriteLock when:

  • Need quorum-based distributed consistency
  • Require fault tolerance (multi-master)
  • Read-heavy workloads with strong consistency
  • Can tolerate higher latency
  • Don't need lock upgrade/downgrade

Choose Redisson RedissonReadWriteLock when:

  • Single Redis instance is acceptable
  • Need high throughput / low latency
  • Require lock upgrade/downgrade
  • Need async/reactive APIs
  • Want pub/sub notifications
  • Need fair ordering (use RedissonFairReadWriteLock)

Conclusion

Both implementations provide distributed read-write locks with different trade-offs:

redlock4j RedlockReadWriteLock: - Counter-based with quorum safety - Higher latency but fault-tolerant - Polling-based waiting - No lock conversion support - Best for multi-master setups requiring strong consistency

Redisson RedissonReadWriteLock: - Hash-based with atomic Lua scripts - Lower latency but single point of failure - Pub/sub notifications - Lock upgrade/downgrade support - Best for high-throughput single-instance deployments

Choose based on your specific requirements: - Distributed consistency & fault tolerance → redlock4j - High throughput & low latency → Redisson - Fair ordering → Redisson RedissonFairReadWriteLock