Three Ways to Implement Distributed Locks: Database, Redis, and Zookeeper
This article explains three practical implementations of distributed locks—using a relational database, Redis cache, and Zookeeper—detailing their mechanisms, Java code examples, advantages, disadvantages, and comparative analysis of performance and reliability.
Distributed locks are essential for ensuring data consistency in distributed systems, and they can be implemented using three main approaches: database‑based locks, cache (Redis)‑based locks, and Zookeeper‑based locks.
Database implementation uses pessimistic locking with SELECT … FOR UPDATE and optimistic locking via a version field, emphasizing that the lock column must be indexed to avoid full‑table locks.
Redis implementation relies on the SETNX command combined with EXPIRE to create a lock with a UUID value, includes an acquisition timeout, and safely releases the lock using WATCH / MULTI / EXEC transactions.
/**
* Distributed lock simple implementation code 4 */
public class DistributedLock {
private final JedisPool jedisPool;
public DistributedLock(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
/**
* Acquire lock
* @param lockName lock key
* @param acquireTimeout timeout for acquiring
* @param timeout lock expiration time
* @return lock identifier
*/
public String lockWithTimeout(String lockName, long acquireTimeout, long timeout) {
Jedis conn = null;
String retIdentifier = null;
try {
conn = jedisPool.getResource();
String identifier = UUID.randomUUID().toString();
String lockKey = "lock:" + lockName;
int lockExpire = (int) (timeout / 1000);
long end = System.currentTimeMillis() + acquireTimeout;
while (System.currentTimeMillis() < end) {
if (conn.setnx(lockKey, identifier) == 1) {
conn.expire(lockKey, lockExpire);
retIdentifier = identifier;
return retIdentifier;
}
if (conn.ttl(lockKey) == -1) {
conn.expire(lockKey, lockExpire);
}
try { Thread.sleep(10); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
} catch (JedisException e) { e.printStackTrace(); }
finally { if (conn != null) { conn.close(); } }
return retIdentifier;
}
/**
* Release lock
* @param lockName lock key
* @param identifier lock identifier
* @return success flag
*/
public boolean releaseLock(String lockName, String identifier) {
Jedis conn = null;
String lockKey = "lock:" + lockName;
boolean retFlag = false;
try {
conn = jedisPool.getResource();
while (true) {
conn.watch(lockKey);
if (identifier.equals(conn.get(lockKey))) {
Transaction transaction = conn.multi();
transaction.del(lockKey);
List<Object> results = transaction.exec();
if (results == null) { continue; }
retFlag = true;
}
conn.unwatch();
break;
}
} catch (JedisException e) { e.printStackTrace(); }
finally { if (conn != null) { conn.close(); } }
return retFlag;
}
}The article also provides a test harness that simulates a flash‑sale scenario with 50 concurrent threads, showing ordered output when the lock works and unordered output when the lock is disabled.
public class Service {
private static JedisPool pool = null;
private DistributedLock lock = new DistributedLock(pool);
int n = 500;
static {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(200);
config.setMaxIdle(8);
config.setMaxWaitMillis(1000 * 100);
config.setTestOnBorrow(true);
pool = new JedisPool(config, "127.0.0.1", 6379, 3000);
}
public void seckill() {
String identifier = lock.lockWithTimeout("resource", 5000, 1000);
System.out.println(Thread.currentThread().getName() + "获得了锁");
System.out.println(--n);
lock.releaseLock("resource", identifier);
}
} public class ThreadA extends Thread {
private Service service;
public ThreadA(Service service) { this.service = service; }
@Override
public void run() { service.seckill(); }
}
public class Test {
public static void main(String[] args) {
Service service = new Service();
for (int i = 0; i < 50; i++) {
ThreadA threadA = new ThreadA(service);
threadA.start();
}
}
}Zookeeper implementation creates an ephemeral sequential node under a designated lock directory; the client that creates the smallest node obtains the lock, while others watch the next smallest node. The Apache Curator library’s InterProcessMutex simplifies this process.
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.RetryNTimes;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class ZkLock implements DistributionLock {
private String zkAddress = "zk_adress";
private static final String root = "package root";
private CuratorFramework zkClient;
private final String LOCK_PREFIX = "/lock_";
@Bean
public DistributionLock initZkLock() {
if (StringUtils.isBlank(root)) { throw new RuntimeException("zookeeper 'root' can't be null"); }
zkClient = CuratorFrameworkFactory.builder()
.connectString(zkAddress)
.retryPolicy(new RetryNTimes(2000, 20000))
.namespace(root)
.build();
zkClient.start();
return this;
}
public boolean tryLock(String lockName) {
lockName = LOCK_PREFIX + lockName;
boolean locked = true;
try {
Stat stat = zkClient.checkExists().forPath(lockName);
if (stat == null) {
log.info("tryLock:{}", lockName);
stat = zkClient.checkExists().forPath(lockName);
if (stat == null) {
zkClient.create().creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL)
.forPath(lockName, "1".getBytes());
} else {
log.warn("double-check stat.version:{}", stat.getAversion());
locked = false;
}
} else {
log.warn("check stat.version:{}", stat.getAversion());
locked = false;
}
} catch (Exception e) { locked = false; }
return locked;
}
public void release(String lockName) {
lockName = LOCK_PREFIX + lockName;
try {
zkClient.delete().guaranteed().deletingChildrenIfNeeded().forPath(lockName);
log.info("release:{}", lockName);
} catch (Exception e) { log.error("删除", e); }
}
public void setZkAddress(String zkAddress) { this.zkAddress = zkAddress; }
}Advantages and disadvantages are compared: database locks have poor performance and risk of table‑level locking; Redis locks are fast but can suffer from lock‑deletion failures and require careful timeout handling; Zookeeper offers high reliability and re‑entrancy but incurs higher latency due to leader coordination.
The article concludes with a summary table comparing difficulty, implementation complexity, performance, and reliability of the three approaches.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architecture Digest
Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
