What is a Redis lock?
Software developers and sysadmins will be familiar with the concept of locks. In the world of applications and services like databases, a lock is a fundamental tool for managing access to shared resources. A lock ensures that only one process or thread at a time can modify a data resource.
Locks are essential for preventing conflicts and ensuring data integrity. This is especially true in distributed systems, where your application runs across multiple servers. With distributed systems, multiple instances of your application may try to access or update the same resource simultaneously. Traditional lock mechanisms won't suffice in these situations. Instead, you need distributed locks.
The need for distributed locks
To understand the need for distributed locks, imagine a distributed eCommerce application for a worldwide retailer. The seller's eCommerce application is distributed to regional clusters to improve speed and responsiveness for customers worldwide. Without distributed locking, if one customer in North America and another in Europe both attempted to purchase the last available stock of a particular item, both orders could go through.
Now consider a distributed document-based application. Without distributed locking, two separate users could make changes simultaneously, leading to inconsistent results with no data integrity. Distributed locks ensure that only one user at a time can edit a document, even if multiple users log into different regional clusters.
In programming terms, distributed locks prevent race conditions. In a race condition, multiple processes access and attempt to modify the same resource simultaneously, resulting in unpredictable outcomes. Distributed locks coordinate tasks, synchronize access, and ensure data consistency across distributed systems.
Distributed locking concepts
Distributed locking builds upon basic programmatic lock principles and adapts them to a distributed environment. Although distributed locking can be implemented in any number of ways, each method implements three key concepts.
First is lock acquisition. Before a process can modify a shared resource, it must acquire a lock. In the case of multiple processes requesting a lock, the first process to obtain the lock retains exclusive access to the shared resource.
The process holding the lock then enters the critical section. At this stage, the process can safely perform operations on the shared resources. No other process can enter the critical section until the lock is released.
Once the process completes its task, it releases the lock, making lock release the final key concept. After the lock release, other processes can attempt to acquire the lock and proceed with their tasks.
Other essential concepts apply to both standard locks and distributed locking. This includes the lock timeout concept, which sets a time limit for the life of a lock, preventing a process from holding a lock indefinitely. This ensures other processes can eventually acquire the lock, even if the process holding the lock fails.
Fairness is another standard locking concept, although it's not necessarily implemented in all locking methods. If fairness is implemented, it prevents a process from repeatedly acquiring a lock when others are waiting. Many distributed locking systems don't guarantee fairness by default.
Implementing locks with Redis
As the data store for many high-performance distributed applications, Redis provides a lock mechanism that works across a distributed infrastructure. Among its features are lock acquisition, a lock ownership process for the critical section, a lock expiration feature implemented as a time-to-live (TTL) value, and lock release.
Redis has other features to ensure the integrity and atomicity of its distributed locks. Each process requesting a lock must be unique, which is validated by a UUID—a combination of process ID and timestamp. This ensures that only the rightful process owner can release the lock. Redis commands like SETNX ("Set if Not Exists") with an expiration time ensure that lock acquisition and release are atomic operations, avoiding race conditions.
Here's a sample SETNX command:
SET resource_name my_random_value NX PX 30000
In this example:
- SETNX sets the resource_name key only if it doesn't already exist.
- My_random_value is a unique value that identifies the lock owner.
- PX 30000 represents the lock's expiration time (30 seconds)
Releasing locks can be achieved in various ways, such as with a Lua script:
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
In this Lua example, you only delete the lock if you're the owner (based on the my_random_value).
Redis locks on Java with Redisson
Java developers can set and release Redis distributed locks via familiar classes and methods with the Redisson library. Redisson provides a high-level abstraction layer over Redis locks, making lock acquisition, the critical section, and lock release simple in any scenario.
Here's a look at the wide range of different lock implementations offered by Redisson—all of which are distributed.
Lock
The Redisson lock mechanism (RLock
) uses the pubsub channel to notify other threads waiting to acquire a lock across all Redisson instances.
If a Redisson instance that acquired a lock crashes, it could hang forever in the acquired state. To avoid this, Redisson maintains a lock watchdog, which prolongs lock expiration while the lock holder Redisson instance is still alive. By default, the lock watchdog timeout is 30 seconds, but this can be changed through the lockWatchdogTimeout setting.
Here's a Java code sample:
RLock lock = redisson.getLock("myLock"); // traditional lock method lock.lock(); // or acquire the lock and automatically unlock it after 10 seconds lock.lock(10, TimeUnit.SECONDS); // or wait for lock acquisition up to 100 seconds // and automatically unlock it after 10 seconds boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS); if (res) { try { ... } finally { lock.unlock(); } }
Here, the leaseTime parameter is defined during lock acquisition. After the specified time interval, the lock is automatically released.
The RLock
object behaves according to the Java Lock specification. This means only the lock owner thread can unlock it. Otherwise, an exception (IllegalMonitorStateException
) is thrown.
Fair lock
The Redisson fair lock feature (FairLock
) guarantees that threads will acquire locks in the requested order. All waiting threads are queued, and if a thread has died, Redisson waits for its return for five seconds. For example, if five threads die, then the delay will be 25 seconds.
FairLock
code example:
RLock lock = redisson.getFairLock("myLock"); // traditional lock method lock.lock(); // or acquire the lock and automatically unlock it after 10 seconds lock.lock(10, TimeUnit.SECONDS); // or wait for lock acquisition up to 100 seconds // and automatically unlock it after 10 seconds boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS); if (res) { try { ... } finally { lock.unlock(); } }
MultiLock
The MultiLock
object allows you to group lock objects and handle them as a single lock. Each RLock
object can belong to a different Redisson instance.
MultiLock
behaves according to the Java Lock specification, so only the lock owner thread can unlock it
Code example with three locks:
RLock lock1 = redisson1.getLock("lock1"); RLock lock2 = redisson2.getLock("lock2"); RLock lock3 = redisson3.getLock("lock3"); RLock multiLock = anyRedisson.getMultiLock(lock1, lock2, lock3); // traditional lock method multiLock.lock(); // or acquire the lock and automatically unlock it after 10 seconds multiLock.lock(10, TimeUnit.SECONDS); // or wait for lock acquisition up to 100 seconds // and automatically unlock it after 10 seconds boolean res = multiLock.tryLock(100, 10, TimeUnit.SECONDS); if (res) { try { ... } finally { multiLock.unlock(); } }
Semaphore
Redisson's RSemaphore
implements an object similar to Java's Semaphore class. Semaphores restrict the number of threads that can access a resource. RSemaphore can be used instead of RLock.
Here's a code example:
RSemaphore semaphore = redisson.getSemaphore("mySemaphore"); // acquire a single permit semaphore.acquire(); // or acquire 10 permits semaphore.acquire(10); // or try to acquire a permit boolean res = semaphore.tryAcquire(); // or try to acquire a permit or wait up to 15 seconds boolean res = semaphore.tryAcquire(15, TimeUnit.SECONDS); // or try to acquire 10 permit boolean res = semaphore.tryAcquire(10); // or try to acquire 10 permits or wait up to 15 seconds boolean res = semaphore.tryAcquire(10, 15, TimeUnit.SECONDS); if (res) { try { ... } finally { semaphore.release(); } }
In addition, the RPermitExpirableSemaphore
object implements Semaphores with support for a lease time parameter:
RPermitExpirableSemaphore semaphore = redisson.getPermitExpirableSemaphore("mySemaphore"); semaphore.trySetPermits(23); // acquire permit String id = semaphore.acquire(); // or acquire a permit with lease time in 10 seconds String id = semaphore.acquire(10, TimeUnit.SECONDS); // or try to acquire a permit String id = semaphore.tryAcquire(); // or try to acquire a permit or wait up to 15 seconds String id = semaphore.tryAcquire(15, TimeUnit.SECONDS); // or try to acquire a permit with least time 15 seconds or wait up to 10 seconds String id = semaphore.tryAcquire(10, 15, TimeUnit.SECONDS); if (id != null) { try { ... } finally { semaphore.release(id); } }
To learn more about working with Redis locks and distributed applications, visit the Redisson website today.