Distributed Java Locks With Redis
What Is Distributed Locking?
In a multithreaded program, different threads may require access to the same resources. However, allowing all threads access to a resource at the same time can result in race conditions, bugs, and other unanticipated behavior.
To ensure that no two threads have simultaneous access to the same resource and that the resource is operated upon in a predictable sequence, programmers use a mechanism known as locks. Each thread first acquires the lock, operates on the resource, and finally releases the lock to other threads.
In Java, lock objects are generally more flexible than using synchronized blocks for a number of reasons. First of all, Lock APIs can operate in different methods, while synchronized blocks are fully contained within one method.
Also, if a thread is blocked, there is no way for it to access a synchronized block. With Lock, that thread will only acquire the lock if it's available. This significantly cuts down the time a thread is waiting. In addition, when a thread is waiting, a method can be invoked to interrupt the thread, which isn't possible when a thread is waiting to acquire a synchronized block.
Distributed locking means that you need to consider not just multiple threads or processes, but different clients running on separate machines. These separate servers must coordinate in order to ensure that only one of them is using the resource at any given time.
Redis-Based Tools for Distributed Java Locking
The Redisson framework is a Redis-based In-Memory Data Grid for Java that provides multiple objects for programmers who need to perform distributed locking. Below, we'll discuss each option and the differences between them.
1. Lock
TheRLock
interface implements the java.util.concurrent.locks.Lock
interface in Java. It is a reentrant lock, which means that threads can lock a resource more than one time. A counter variable keeps track of how many times a lock request has been made. The resource is released once the thread makes enough unlock requests and the counter reaches 0.
The following simple code sample demonstrates how to create and initiate a Lock in Redisson:
RLock lock = redisson.getLock("anyLock");
// Most familiar locking method
lock.lock();
try {
...
} finally {
lock.unlock();
}
If the Redisson instance that acquired this lock crashes, then it is possible that the lock will hang forever in this acquired state. In order to avoid this, Redisson maintains a lock "watchdog" that prolongs the expiration of the lock while the Redisson instance holding the lock is still alive. By default, the timeout for this lock watchdog is 30 seconds. This limit can be changed via the Config.lockWatchdogTimeout
setting.
Redisson also allows you to specify theleaseTime
parameter when the lease is acquired. After the specified time interval, the lock will be released automatically:
// Acquire lock and release it automatically after 10 seconds
// if unlock method hasn't been invoked
lock.lock(10, TimeUnit.SECONDS);
// Wait for 100 seconds and automatically unlock it after 10 seconds
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}
Redisson also provides an asynchronous/reactive/rxjava2 interfaces for the Lock
object:
RLock lock = redisson.getLock("anyLock");
lock.lockAsync();
...
// Reactive Stream (Spring Project Reactor implementation)
RLockReactive lock = redissonReactive.getLock("anyLock");
Mono<Void> res = lock.lock();
...
// Reactive Stream (RxJava2 implementation)
RLockReactive lock = redissonRx.getLock("anyLock");
Flowable<Void> res = lock.lock();
...
Because RLock implements the Lock
interface, only the thread that owns the lock is able to unlock the resource. Any attempt to do otherwise will be met with an IllegalMonitorStateException
.
2. FairLock
Like its cousinRLock
, RFairLock
also implements the java.util.concurrent.locks.Lock
interface. By using a FairLock
, you can guarantee that threads will acquire a resource in the same order that they requested it (i.e. a "first in, first out" queue). Redisson gives threads that have died five seconds to restart before the resource is unlocked for the next thread in the queue.
As with RLocks
, creating and initiating a FairLock
is a straightforward process:
RLock lock = redisson.getFairLock("anyLock");
lock.lock();
try {
...
} catch {
lock.unlock();
}
3. ReadWriteLock
Redisson'sRReadWriteLock
implements the java.util.concurrent.locks.ReadWriteLock
interface. In Java, read/write locks are actually a combination of two locks: a read-only lock that can be owned by multiple threads simultaneously, and a write lock that can only be owned by a single thread at once.
The method of creating and initiating a RReadWriteLock
is as follows:
RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
rwlock.readLock().lock();
try {
...
} finally {
rwlock.readLock().lock();
}
rwlock.writeLock().lock();
try {
...
} finally {
rwlock.writeLock().lock();
}
4. RedLock
The RedissonRedLock
object implements the Redlock locking algorithm for using distributed locks with Redis:
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
lock.lock();
try {
...
} finally {
lock.unlock();
}
In the Redlock algorithm, we have a number of independent Redis master nodes located on separate computers or virtual machines. The algorithm attempts to acquire the lock in each of these instances sequentially, using the same key name and random value. The lock is only acquired if the client was able to acquire the lock from the majority of the instances quicker than the total time for which the lock is valid.
5. MultiLock
The RedissonMultiLock
object is capable of grouping together multiple separate RLock
instances and managing them as a single entity:
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");
RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
lock.lock();
try {
...
} finally {
lock.unlock();
}
As we can see in the example above, eachRLock
object may belong to a different Redisson instance. This, in turn, may connect to a different Redis database.
Final Thoughts
In this article, we have explored some of the different tools that Java developers have available for performing distributed locking in the Redisson framework on top of Redis database: Lock
, FairLock
, ReadWriteLock
, RedLock
, and MultiLock
. For more information about distributed computing in Redisson, follow the project wiki on GitHub.