Distributed lock with Redis and Spring Boot

Egor Ponomarev
3 min readAug 19, 2022

The purpose of locks is to provide mutually exclusive access to a resource. Typically, the lock is used for changing the state of shared resources, not for reading it. One of the frequent use cases for the distributed lock is when the app is run in multiple instances which work with a shared resource and need to update its state with a restriction: only one of them should be able to update it.

Each lock has a name and instances get a lock and unlock it via its name. One of the instances (which manages to acquire the lock) performs an update of a shared resource and after releases the lock.

Distributed lock with Spring Integration Redis and Lettuce

Lettuce is a scalable thread-safe Redis client based on netty and Reactor. Lettuce provides synchronous, asynchronous and reactive APIs to interact with Redis.

I won’t provide here the configuration of RedisConnectionFactory for Lettuce since there are a lot of examples of it.

Framework “Spring Integration” provides the RedisLockRegistry using which you can obtain locks and release them. So first of all you should create a bean of RedisLockRegistry, specifying “Time to Live” (TTL) and the name of the key.

Now let’s implement the logic of acquiring and releasing the lock:

How does it work?

As the documentation says, there can be configured 2 types of locks (both Reentrant ones)

  • RedisLockType.SPIN_LOCK - the lock is acquired by periodic loop (100ms) checking whether the lock can be acquired. Default.
  • RedisLockType.PUB_SUB_LOCK - The lock is acquired by redis pub-sub subscription.

The pub-sub is preferred mode — less network chatter between client Redis server, and more performant — the lock is acquired immediately when subscription is notified about unlocking in the other process. However, the Redis does not support pub-sub in the Master/Replica connections (for example in AWS ElastiCache environment), therefore a busy-spin mode is chosen as a default to make the registry working in any environment.

I will talk here about the default type of lock SPIN_LOCK.

Each object LockRegistry has a random id of type UUID and contains a map of locks (lock name / lock object) that are called locks and that are held by the current instance. When we do lockRegistry.obtain(lockKey), lockRegistry first checks if the map contains this lock (in other words, if the current instance has the lock with the same name acquired at this moment), if so then this lock is returned. Otherwise, lockRegistry checks if Redis contains the key with the name “registry key:lock name” and if the value equals the id of the lockRegistry object. If so, it returns this lock, else it tries to create the key “registry key:lock name” in Redis. When you got a lock and call lock.tryLock() basically the similar steps are performed as in lockRegistry.obtain(lockKey), first the map locks with locks is checked, then Redis.

In some situations, you might need to acquire a lock but not release it. For example, you have a @Scheduled task that runs each 15 seconds on each instance and you don’t want it to be run more often than once per 15 seconds.
To do it you can get a lock and exit from a method without releasing. In such cases, I suggest calling lockRegistry.expireUnusedOlderThan(TTL) each time before obtaining a lock (actually it is better to call it for all cases). This method removes old not released locks from the map locks and prevents the situation when one instance has a map with old locks and all threads of this instance (except the one which acquired this lock) cannot acquire it.

References

https://docs.spring.io/spring-integration/reference/html/redis.html

https://redis.io/docs/reference/patterns/distributed-locks/

--

--