面试题-一般实现分布式锁都有哪些方式?使用 Redis 如何设计分布式锁?使用 zk 来设计分布式锁可以吗?这两种分布式锁的实现方式哪种效率比较高?
一般实现分布式锁有哪些方式?
- 使用 Redis 设计分布式锁的方法。
- 使用 Zookeeper (zk) 设计分布式锁的方法。
- Redis 和 zk 的分布式锁实现方式,哪种效率更高?
面试官心理分析
面试官通常以这种方式提问,先问你对 zk 的了解,逐步过渡到 zk 在分布式锁中的应用。这是因为分布式锁在分布式系统开发中的使用场景非常常见。
面试题剖析
Redis 分布式锁
Redis 提供了一种官方支持的分布式锁算法——RedLock,其核心要求如下:
- 互斥性:只有一个客户端能获取锁。
- 避免死锁:锁必须自动释放。
- 容错性:即使部分 Redis 节点发生故障,仍然可以获取锁。
1. Redis 普通分布式锁
利用 Redis 的 SET
指令实现:
SET key value [EX seconds] [PX milliseconds] NX
参数说明:
NX
:只有在键不存在时才会设置成功。EX seconds
:设置键的过期时间(秒级)。PX milliseconds
:设置键的过期时间(毫秒级)。
示例:
SET resource_name my_random_value PX 30000 NX
释放锁:
使用 Lua 脚本保证只有当前持有锁的客户端才能释放锁:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
2. RedLock 算法
RedLock 基于 Redis 集群设计,假设有 5 个 Redis master 节点,获取锁的步骤如下:
- 获取当前时间戳(毫秒)。
- 在每个 master 节点尝试创建锁,超时时间较短(如 10~50 毫秒)。
- 成功在大多数节点(
n/2+1
)上创建锁。 - 计算锁创建耗时,确认是否在有效时间内完成。
- 若锁创建失败,依次释放已创建的锁。
- 其他客户端需要轮询重试。
详细说明参考:Redis 官方文档。
Zookeeper (zk) 分布式锁
zk 实现方式 1:临时节点
通过创建临时节点实现分布式锁:
- 客户端尝试创建临时 znode。
- 创建成功则获取锁,创建失败则注册监听器等待锁释放。
- 释放锁时删除 znode,通知等待的客户端。
示例代码:
zookeeper.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
zk 实现方式 2:临时顺序节点
利用临时顺序节点,可以实现公平锁:
- 每个客户端创建一个临时顺序节点。
- 获取锁的客户端为序号最小的节点。
- 非最小序号的节点监听其前一个节点的状态。
- 节点释放锁时,通知下一个节点获取锁。
示例代码:
/**
* ZooKeeperSession
*/
public class ZooKeeperSession {
private static CountDownLatch connectedSemaphore = new CountDownLatch(1);
private ZooKeeper zookeeper;
private CountDownLatch latch;
public ZooKeeperSession() {
try {
this.zookeeper = new ZooKeeper("192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181", 50000, new ZooKeeperWatcher());
try {
connectedSemaphore.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ZooKeeper session established......");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取分布式锁
*
* @param productId
*/
public Boolean acquireDistributedLock(Long productId) {
String path = "/product-lock-" + productId;
try {
zookeeper.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
return true;
} catch (Exception e) {
while (true) {
try {
// 相当于是给node注册一个监听器,去看看这个监听器是否存在
Stat stat = zk.exists(path, true);
if (stat != null) {
this.latch = new CountDownLatch(1);
this.latch.await(waitTime, TimeUnit.MILLISECONDS);
this.latch = null;
}
zookeeper.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
return true;
} catch (Exception ee) {
continue;
}
}
}
return true;
}
/**
* 释放掉一个分布式锁
*
* @param productId
*/
public void releaseDistributedLock(Long productId) {
String path = "/product-lock-" + productId;
try {
zookeeper.delete(path, -1);
System.out.println("release the lock for product[id=" + productId + "]......");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 建立 zk session 的 watcher
*/
private class ZooKeeperWatcher implements Watcher {
public void process(WatchedEvent event) {
System.out.println("Receive watched event: " + event.getState());
if (KeeperState.SyncConnected == event.getState()) {
connectedSemaphore.countDown();
}
if (this.latch != null) {
this.latch.countDown();
}
}
}
/**
* 封装单例的静态内部类
*/
private static class Singleton {
private static ZooKeeperSession instance;
static {
instance = new ZooKeeperSession();
}
public static ZooKeeperSession getInstance() {
return instance;
}
}
/**
* 获取单例
*
* @return
*/
public static ZooKeeperSession getInstance() {
return Singleton.getInstance();
}
/**
* 初始化单例的便捷方法
*/
public static void init() {
getInstance();
}
}
也可以采用另一种方式,创建临时顺序节点:
如果有一把锁,被多个人给竞争,此时多个人会排队,第一个拿到锁的人会执行,然后释放锁;后面的每个人都会去监听排在自己前面的那个人创建的 node 上,一旦某个人释放了锁,排在自己后面的人就会被 ZooKeeper 给通知,一旦被通知了之后,就 ok 了,自己就获取到了锁,就可以执行代码了。
public class ZooKeeperDistributedLock implements Watcher {
private ZooKeeper zk;
private String locksRoot = "/locks";
private String productId;
private String waitNode;
private String lockNode;
private CountDownLatch latch;
private CountDownLatch connectedLatch = new CountDownLatch(1);
private int sessionTimeout = 30000;
public ZooKeeperDistributedLock(String productId) {
this.productId = productId;
try {
String address = "192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181";
zk = new ZooKeeper(address, sessionTimeout, this);
connectedLatch.await();
} catch (IOException e) {
throw new LockException(e);
} catch (KeeperException e) {
throw new LockException(e);
} catch (InterruptedException e) {
throw new LockException(e);
}
}
public void process(WatchedEvent event) {
if (event.getState() == KeeperState.SyncConnected) {
connectedLatch.countDown();
return;
}
if (this.latch != null) {
this.latch.countDown();
}
}
public void acquireDistributedLock() {
try {
if (this.tryLock()) {
return;
} else {
waitForLock(waitNode, sessionTimeout);
}
} catch (KeeperException e) {
throw new LockException(e);
} catch (InterruptedException e) {
throw new LockException(e);
}
}
public boolean tryLock() {
try {
// 传入进去的locksRoot + “/” + productId
// 假设productId代表了一个商品id,比如说1
// locksRoot = locks
// /locks/10000000000,/locks/10000000001,/locks/10000000002
lockNode = zk.create(locksRoot + "/" + productId, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// 看看刚创建的节点是不是最小的节点
// locks:10000000000,10000000001,10000000002
List<String> locks = zk.getChildren(locksRoot, false);
Collections.sort(locks);
if (lockNode.equals(locksRoot + "/" + locks.get(0))) {
// 如果是最小的节点,则表示取得锁
return true;
}
// 如果不是最小的节点,找到比自己小1的节点
int previousLockIndex = -1;
for (int i = 0; i < locks.size(); i++) {
if (lockNode.equals(locksRoot + "/" +locks.get(i))){
previousLockIndex = i - 1;
break;
}
}
this.waitNode = locks.get(previousLockIndex);
} catch (KeeperException e) {
throw new LockException(e);
} catch (InterruptedException e) {
throw new LockException(e);
}
return false;
}
private boolean waitForLock(String waitNode, long waitTime) throws InterruptedException, KeeperException {
Stat stat = zk.exists(locksRoot + "/" + waitNode, true);
if (stat != null) {
this.latch = new CountDownLatch(1);
this.latch.await(waitTime, TimeUnit.MILLISECONDS);
this.latch = null;
}
return true;
}
public void unlock() {
try {
// 删除/locks/10000000000节点
// 删除/locks/10000000001节点
System.out.println("unlock " + lockNode);
zk.delete(lockNode, -1);
lockNode = null;
zk.close();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
public class LockException extends RuntimeException {
private static final long serialVersionUID = 1L;
public LockException(String e) {
super(e);
}
public LockException(Exception e) {
super(e);
}
}
}
但是,使用 zk 临时节点会存在另一个问题:由于 zk 依靠 session 定期的心跳来维持客户端,如果客户端进入长时间的 GC,可能会导致 zk 认为客户端宕机而释放锁,让其他的客户端获取锁,但是客户端在 GC 恢复后,会认为自己还持有锁,从而可能出现多个客户端同时获取到锁的情形。#209
针对这种情况,可以通过 JVM 调优,尽量避免长时间 GC 的情况发生。
Redis 分布式锁与 zk 分布式锁的对比
特性 | Redis 分布式锁 | zk 分布式锁 |
---|---|---|
获取锁性能 | 需要轮询,性能开销较高 | 通过监听器,性能开销较低 |
锁释放机制 | 锁超时自动释放 | 客户端断开,锁自动释放 |
实现复杂度 | 逻辑复杂(如 RedLock) | 实现简单,语义清晰 |
容错性 | 集群 Redis 容错性较高 | zk 的一致性更有保障 |
结论
zk 的分布式锁模型清晰、易用且更稳健,而 Redis 分布式锁在实现上较为复杂,性能和可靠性稍逊一筹。
评论区