Distributed Locks: Ensuring Synchronisation in Distributed Systems
Ensuring proper synchronisation among various components is crucial for maintaining data integrity and preventing race conditions. One essential tool in achieving this synchronisation is distributed locks.
In this blog post, we’ll explore into the concept of distributed locks and examine some popular implementations.
What are Distributed Locks ?
Essentially, a distributed lock is a basic for synchronisation that enables concurrent processes or nodes to coordinate access to a shared resource or crucial area in a distributed system. Distributed locks assist in avoiding conflicts and preserving consistency by prohibiting several processes from accessing a resource concurrently, much like traditional locks in single-threaded or multi-threaded contexts.
Why Distributed Locks ?
In a distributed environment where multiple nodes or processes operate independently, ensuring coordinated access to shared resources becomes challenging. Without proper synchronisation mechanisms like distributed locks, the system risks data corruption, inconsistencies, and even deadlock situations.
Distributed locks play a vital role in various scenarios, including:
- Concurrency Control: Coordinating access to shared data structures to prevent race conditions and maintain consistency.
- Resource Management: Managing access to limited resources such as database connections, file systems, or network resources.
- Distributed Coordination: Facilitating distributed algorithms and protocols like distributed transactions, leader election, and distributed consensus.
Some of the Implementations
Several distributed lock implementations exist, each with its own set of features, trade-offs, and use cases. Some of the popular ones include:
- ZooKeeper: Apache ZooKeeper is a centralised coordination service. It offers a high level of reliability and consistency but introduces a single point of failure.
- Redis: Redis, a popular in-memory data store, offers a simple yet effective distributed locking mechanism using its
SETNX
(set if not exists) command or itsSET
command withNX
option. Redis-based locks are lightweight and widely used but may lack features like reentrancy and timeout. - etcd: As a distributed key-value store, etcd offers distributed locking primitives through its transactional capabilities. It provides strong consistency guarantees and is commonly used in Kubernetes and other distributed systems.
Let’s explore two common implementations of distributed locks: using Redis and Apache ZooKeeper.
# Redis Distributed Locks (Python Example)
python
import redis
import time
class RedisDistributedLock:
def __init__(self, redis_conn, lock_key):
self.redis_conn = redis_conn
self.lock_key = lock_key
def acquire(self, timeout=10):
start_time = time.time()
while time.time() - start_time < timeout:
if self.redis_conn.set(self.lock_key, "locked", nx=True, ex=timeout):
return True
time.sleep(0.1)
return False
def release(self):
self.redis_conn.delete(self.lock_key)
# Example Usage
redis_conn = redis.Redis(host='localhost', port=6379, db=0)
lock = RedisDistributedLock(redis_conn, "my_lock_key")
if lock.acquire():
try:
# Critical section
print("Executing critical section...")
time.sleep(5)
finally:
lock.release()
else:
print("Failed to acquire lock.")
Apache ZooKeeper Distributed Locks (Java Example)
java
import org.apache.zookeeper.*;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
public class ZooKeeperDistributedLock {
private static final String ZOOKEEPER_ADDRESS = "localhost:2181";
private static final int SESSION_TIMEOUT = 5000;
private static final String LOCK_PATH = "/my_lock";
private ZooKeeper zooKeeper;
private String lockPath;
public ZooKeeperDistributedLock() throws IOException, InterruptedException, KeeperException {
final CountDownLatch connectedSignal = new CountDownLatch(1);
zooKeeper = new ZooKeeper(ZOOKEEPER_ADDRESS, SESSION_TIMEOUT, watchedEvent -> {
if (watchedEvent.getState() == Watcher.Event.KeeperState.SyncConnected) {
connectedSignal.countDown();
}
});
connectedSignal.await();
if (zooKeeper.exists(LOCK_PATH, false) == null) {
zooKeeper.create(LOCK_PATH, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
}
public void acquire() throws KeeperException, InterruptedException {
lockPath = zooKeeper.create(LOCK_PATH + "/", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
while (true) {
String minChild = zooKeeper.getChildren(LOCK_PATH, false).stream().min(String::compareTo).orElse("");
if (lockPath.endsWith(minChild)) {
return;
} else {
CountDownLatch latch = new CountDownLatch(1);
zooKeeper.exists(LOCK_PATH + "/" + minChild, watchedEvent -> {
if (watchedEvent.getType() == Watcher.Event.EventType.NodeDeleted) {
latch.countDown();
}
});
latch.await();
}
}
}
public void release() throws KeeperException, InterruptedException {
zooKeeper.delete(lockPath, -1);
}
public void close() throws InterruptedException {
zooKeeper.close();
}
// Example Usage
public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
ZooKeeperDistributedLock lock = new ZooKeeperDistributedLock();
lock.acquire();
try {
// Critical section
System.out.println("Executing critical section...");
Thread.sleep(5000);
} finally {
lock.release();
lock.close();
}
}
}
Conclusion
Distributed locks are essential instruments for guaranteeing synchronisation and coordination. By providing mechanisms for exclusive access to shared resources, they help maintain data integrity, prevent race conditions, and facilitate distributed coordination. Understanding the concepts, implementations, and best practices surrounding distributed locks is essential for building robust and reliable distributed systems. Whether you’re designing a microservices architecture, building cloud-native applications, or orchestrating containerised workloads, distributed locks play a crucial role in achieving consistency and reliability.