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

TheRLockinterface 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 theleaseTimeparameter 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: LockFairLockReadWriteLockRedLock, and MultiLock. For more information about distributed computing in Redisson, follow the project wiki on GitHub.

 

 

 

 

Top