Home

Comparing RedisLockService to Redisson's lock implementation

2026-10-04

In this post, we look at how RedisLockService compares to Redisson's lock implementation.

Redisson is probably the most used Redis-based distributed locking library for the JVM. It is mature and it also supports pretty much every Redis setup out there.

So the question which needs to be addressed is: Why does RedisLockService exist? It turns out that both implementations solve the same problem, but as you will see, they make quite different trade-offs.

Core mechanics

Both implementations need to read and modify a lock entry atomically. But they solve this in different ways:

Redisson RedisLockService
Atomicity Lua scripts (EVAL), executed atomically by Redis Optimistic transactions (WATCH / MULTI / EXEC) on a single Redis connection
Lock entry A Redis hash per lock: <clientId>:<threadId>. Upon reentrancy, a counter is increased, which means another round trip to Redis A JSON record per lock containing all info: write owner, read owners, lease expiry and usage data. It is intended to be read for insights and analysis
Lease expiry Key TTL (PEXPIRE), enforced by Redis, i.e. the entry is deleted by Redis The JSON entry contains the expiry. The JSON entry can (optionally) remain stored in Redis for insights and analysis.

Redisson's Lua scripts have one clear advantage: they never conflict. A script runs to completion before Redis serves anything else, so there is no need to retry. RedisLockService uses plain transactions instead, which means that the decision is made in the application. If another client modifies the same lock entry in between, the transaction is aborted and retried. With many concurrent readers on the same lock this can lead to contention.

Making the decision in the JVM code, rather than in Lua, is what makes the more advanced features (see below) possible - and it keeps all the logic in one language, testable with normal unit tests.

Acquiring several locks at once

Often, the protected code needs more than one lock. For example in a car-rental application both the customer-id and the vehicle-id might be needed to both validate car availability and make the reservation in one single atomic transaction.

Redisson offers RedissonMultiLock for this. It acquires the locks one by one and releases the already acquired ones if a later one fails. While it is doing so, a thread can hold some of the locks while waiting for the rest, which blocks others unnecessarily and is prone to deadlocks.

RedisLockService acquires all requested locks in one transaction: either all of them, or none ✅. A thread never sits on half of its locks while waiting for the other half.

API: Callback vs. lock/unlock

Redisson implements the well-known java.util.concurrent.locks.Lock interface (lock(), tryLock(), unlock()), plus async, reactive and RxJava variants. This is familiar and flexible: a lock can be released in a different method than where it was acquired.

RedisLockService deliberately only offers a callback-based API:

val (acquired, result) = redisLockService.tryWriteLock(listOf("customer-42", "invoice-7"), timeoutMs = 10_000) { hasLock ->
    doWork()
    if (!hasLock()) return@tryWriteLock null // no longer safe to proceed
    "done"
}

The locks are always released when the protected code returns or throws - a forgotten unlock() simply cannot happen ✅.

Knowing when a lock is lost

A distributed lock can be lost while it is being held: the lease could not be renewed in time (network issues, a paused JVM), or Redis restarted and lost its data. The question is whether the protected code finds out.

Redisson does not actively tell you. You can call isHeldByCurrentThread(), which is a round trip to Redis, but most code never does.

RedisLockService hands the protected code a hasLock() function. It is a cheap, local check which turns false as soon as the lock is considered lost - because the lease ran out without a successful renewal, the entry was taken over, or a Redis restart was detected ✅. Long-running code can check it between steps and wind down in time.

Lease renewal

Both implementations automatically extend the lease while the lock is held. Redisson's "watchdog" (by default a 30-second lease, renewed every 10 seconds) is however only active when no explicit lease time is given. With an explicit lease time, the lock simply expires, whether the protected code is done or not.

RedisLockService always renews the lease while the protected code runs ✅. Failed renewals are retried every second until the lease runs out, after which the lock is marked as lost (see hasLock() above).

Reentrancy

Both implementations are reentrant per thread. Redisson keeps the reentrancy counter in Redis, so every nested lock and unlock costs a round trip. RedisLockService keeps track of the locks held by the current thread locally, so nested calls on locks that are already held do not touch Redis at all ✅.

Read/write locks

Both implementations offer read (shared) and write (exclusive) locks, but they behave differently:

  • Writer starvation: Redisson's RReadWriteLock grants new read locks as long as no write lock is held. With a steady stream of overlapping readers, a writer might never get its turn. RedisLockService prefers writers: while a thread waits for a write lock, no new read locks of the same name are granted. The current readers finish, and then the writer gets the lock ✅.
  • Upgrading: in Redisson, a thread holding a read lock that requests the write lock of the same name deadlocks with itself. In RedisLockService, a nested call can upgrade a read lock to a write lock, which is granted once no other thread holds the read lock ✅.

Deadlock detection

Redisson has no deadlock detection: two threads waiting for each other's locks simply block until their timeouts run out (or forever, with lock()).

RedisLockService detects deadlocks across JVMs ✅. Waiting threads broadcast which locks they hold and which they wait for. When a thread finds a cycle leading back to itself, exactly one thread in that cycle gives up with a RedisLockServiceDeadlockException, so the others can continue. Since deadlocks usually reveal coding errors, an exception in your logs is a lot more helpful than a mysterious timeout.

Redis restarts

This is where the two implementations differ the most. When Redis restarts without (complete) persistence, the lock entries are gone. With Redisson, the next thread asking for the lock gets it - while the previous owner still believes it holds it. Two threads now run the protected code at the same time. ❌

RedisLockService detects restarts using Redis' uptime ✅. After a (re)start, no lock is handed out until Redis has been up for at least the lease time. Within that time, the previous owners notice the restart during their lease renewal and back down (hasLock() returns false). I described this in more detail in the previous post.

The same problem exists for replicated setups, i.e. Redis Cluster or Sentinel, which Redisson supports. Redis replicates asynchronously, so when the primary fails, the promoted replica might not have received the latest lock writes yet. Again, two threads can end up holding the same lock ❌. Redisson can wait for lock writes to reach the replicas (WAIT), which makes the window smaller, but WAIT does not turn Redis into a strongly consistent system: a replica which did not receive the write can still be promoted. So while Redisson runs on these setups, it cannot guarantee mutual exclusion on them.

RedisLockService avoids this: it is designed for a single, standalone Redis instance only. And on that setup, it is safe ✅. A single instance has no replicas which could fall behind - every lock write is seen by every following read. Each lock is granted in a transaction which only commits if the lock entry has not changed since it was read, so two threads can never be granted the same lock at the same time. The one event which could still erase the lock entries - a restart - is detected and handled as described above. And should a lease ever run out, for example because the holder could not reach Redis for too long, the protected code finds out through hasLock().

If your locks must remain available when a Redis node fails, consider a system built on a consensus protocol, such as etcd or ZooKeeper.

Where Redisson is the better choice

To be fair, there are good reasons to choose Redisson:

  • Best-effort locking on a replicated Redis: If you cannot spin up a single-instance Redis and must use Redis Cluster or with Sentinel, and the lock is merely an optimization - e.g. to avoid doing the same work twice - an occasional double grant after a failover may be acceptable. Redisson runs on these setups, RedisLockService does not. Just be aware that the lock then cannot be relied on for correctness (see Redis restarts).
  • No contention: Lua scripts never need to be retried. RedisLockService's optimistic transactions can suffer from contention when many threads read-lock the same name.
  • Fair locks: Redisson offers RFairLock, which hands out a lock in the order it was requested.
  • Flexible API: the Lock interface and the async/reactive variants fit codebases where a lock cannot be scoped to a single block of code.
  • Maturity: Redisson has been battle-tested in production for many years by a large user base.

Summary

Redisson is a general-purpose toolbox which covers almost every Redis setup and locking style. RedisLockService is narrower in scope: it targets a single, standalone Redis instance. Within that scope, however, it goes further on correctness:

  • Several locks acquired atomically: all or none.
  • The protected code knows when its lock is lost.
  • Writers are not starved by readers, and read locks can be upgraded.
  • Deadlocks across JVMs are detected and broken up.
  • Redis restarts do not lead to two threads holding the same lock.
  • Locks are always released, as the API is scoped to the protected code.

If you can run a standalone Redis and correctness of your locks matters more to you than support for every topology, RedisLockService is worth a look. The code is released under the MIT license as part of the kotlin-redis client library, and the documentation can be found here.

Happy locking.