Sitemap

Distributed Locking using Redis

5 min readJun 29, 2025

--

In a single-instance app, locking is easy — just use synchronized or a Java ReentrantLock and problem is solved, but what if you have multiple instances ? — The solution is a distributed lock.

A distributed lock is a locking mechanism that ensures only one instance in a distributed system can hold a lock on a resource at a given time — even though multiple instances can try to acquire it simultaneously, multiple app instances may:

  • Run the same scheduled job.
  • Handle the same incoming user request.
  • Try to update the same resource.

Today, we will learn how to implement distributed locking using Redis, focusing on the most common scenarios in distributed systems where locking is essential.

Scenario A: When you have multiple instances of your application running, and you want to ensure that a scheduled job executes on only one instance to avoid duplicate execution.

Scenario B: When multiple concurrent requests for the same operation are sent at the same time, but you need to ensure that the request is processed only once.
For example, in a hotel management system, if multiple cancellation requests for the same reservation are received simultaneously, you want to handle the request only once to prevent double credit being issued to the guest.

First, let’s discuss Scenario A, which can be handled effectively using ShedLock. ShedLock ensures that only one instance of a scheduled task runs across distributed nodes (e.g., in a Spring Boot app with multiple pods/instances). It’s commonly used to prevent duplicate scheduled executions when running in a cluster.

@Scheduled(cron = "0 0 * * * *")
@SchedulerLock(name = "dailyScheduledJob", lockAtMostFor = "10m", lockAtLeastFor = "2m")
public void scheduledJob() {
...
}

Internally, ShedLock:

  • Tries to acquire a lock in Redis using a key like:
    shedlock:dailyScheduledJob
  • If the key does not exist or is expired → it sets the key with a TTL and proceeds to run the job.
  • If the key exists and has not expired → it skips execution (another instance is already running or has run recently).

lockAtMostFor : It ensures the lock exists for at most this time.Even if the task crashes, the lock will auto-expire after this duration.Prevents permanent lockout.

lockAtLeastFor: Keeps the lock for at least this long.Useful for tasks that finish quickly but you don’t want them to run more than once within a certain window.

Image (a) : Shedlock execution example

Let’s discuss Scenario B : We can solve this problem using Redisson.

Redisson is a Java Redis client library that provides:

  • Simple and powerful abstractions for using Redis in distributed Java applications
  • An easy way to use distributed locks, caches, synchronizers, queues, and more — all backed by Redis

It builds on top of the Redis protocol and makes Redis feel like a Java-native concurrent toolkit, especially useful in distributed systems and microservices.

Get Rohit Malhotra’s stories in your inbox

Join Medium for free to get updates from this writer.

It has two main methods :

  1. tryLock(Wait, TTL, Unit) — Use it when duration of task completion is predictable.
  2. lock() — Use it when duration of task completion is not predictable and you want auto renewal of TTL.

Multiple users (or systems) might try to cancel the same booking at the same time. We want to:

  • Allow only one cancellation per booking
  • Prevent double refunds, status inconsistencies, etc.
@Service
public class BookingCancellationService {

private final RedissonClient redissonClient;

public BookingCancellationService(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}

public void cancelWithLock(String bookingId) {
RLock lock = redissonClient.getLock("lock:booking:" + bookingId);
lock.lock(); // waits until the lock is acquired

try {
System.out.println("🔒 Lock acquired for booking: " + bookingId);
// Process cancellation request here
} finally {
lock.unlock();
System.out.println("🔓 Lock released for booking: " + bookingId);
}
}

public void cancelWithTryLock(String bookingId) {
RLock lock = redissonClient.getLock("lock:booking:" + bookingId);

try {
boolean acquired = lock.tryLock(3, 10, TimeUnit.SECONDS); // wait max 3s, lease 10s

if (acquired) {
System.out.println("✅ Lock acquired for booking: " + bookingId);
// Process cancellation request here
} else {
System.out.println("❌ Could not acquire lock. Already being processed.");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
System.out.println("🔓 Lock released for booking: " + bookingId);
}
}
}

}



Image (b) : Current same cancellation request

How tryLock() works in Redisson ?

  • A key like lock:reservation:XYZ123 is created in Redis.
Redis stores:
Key: lock:reservation:XYZ123
Value: <UUID-of-lock-owner>:<timestamp>
TTL: 10 seconds (lease time)
  • If another request tries to acquire the same lock:

a) It will wait up to wait time (tryLock timeout).

b) If not acquired in that time, it skips.

  • Once the task is done, .unlock() deletes the key if the current thread still holds it.

How lock() Works in Redisson?

  • A key is created in Redis:
Key:    lock:reservation:XYZ123
Value: <UUID-of-instance>:<thread-ID>
TTL: Default 30 seconds (renewable)
  • Redisson starts a “watchdog” background thread that automatically extends the TTL while the thread is alive. It will reset the TTL after every 10 seconds while the thread is alive.
  • If another request calls lock() while lock is held: It blocks forever until the current thread releases the lock.
  • After task is completed, unLock() method is called, it will kill the key from the redis is present.

CONCLUSION :

ShedLock is best suited for scenarios where you have scheduled tasks running in a Spring Boot application and need to ensure that the same job is not executed simultaneously by multiple application instances. It works by storing lock metadata in a shared database like PostgreSQL, MySQL, MongoDB, or Redis. When a scheduled task is triggered, ShedLock checks whether the lock already exists — if so, the task is skipped; otherwise, it proceeds and acquires the lock. It is non-blocking, meaning it doesn’t wait for the lock — it simply decides whether or not to run the job. ShedLock is simple to integrate, especially if your app already connects to a database, and is ideal for controlling job execution frequency in clustered deployments.

On the other hand, Redisson is a general-purpose distributed lock library built on top of Redis. It is more versatile and powerful than ShedLock, offering support for both blocking (lock()) and non-blocking (tryLock()) locking mechanisms. Redisson is ideal for use cases where you need to control concurrent access to a shared resource, such as cancellation APIs, payment processing, inventory updates, or idempotency guarantees. It creates Redis keys to represent locks and supports auto-expiry with TTL, as well as a watchdog mechanism to auto-renew locks held by active threads. Redisson is highly performant due to Redis being in-memory, and it ensures thread ownership, meaning a thread can only release its own lock, which adds a layer of safety in concurrent environments.

In summary, ShedLock is ideal for scheduled job deduplication, while Redisson excels in fine-grained, real-time distributed locking scenarios such as API requests. Both tools are valuable and can even be used together in large-scale applications, depending on whether you’re managing periodic jobs or concurrent business operations.

--

--