实现要点
redis分布式锁的问题
redis通常也用来实现分布式锁,但是有一些问题
- 进程需要主动请求redis判断锁是否被释放,会造成服务端的压力以及客户端循环的开销
- 获得锁的进程需要设置过期时间来容错,有产生延迟的风险
- redis主从切换有可能导致锁失效
zookeeper的优势
使用zookeeper实现分布式锁的优势
- 临时节点,如果客户端失活节点被删除,可以通过设置session过期时间来控制删除时间
- 有序节点,严格的单调递增顺序,可以控制并发时的顺序,类似排队
- watch机制,客户端可以监听某个节点的任何事件
- zookeeper集群是几乎高可用的,快速的选主,官方号称200ms内
- zookeeper能够做到对外的统一视图,可线性化,leader节点失效会停止服务
实现思路
客户端创建临时有序节点 /testLock/lock ,然后拿到父目录 /testLock 下的所有节点并排序,判断自己是否是第一个节点,如果是获取锁成功,如果不是,就监听它的前一个节点的删除事件,当监听节点被删除时,获取锁成功。
代码实现
zookeeper集群的搭建和项目搭建细节请参考上一篇文章
ZkUtils文件
package org.example.lock;
import org.apache.zookeeper.ZooKeeper;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
public class ZkUtils {
public static String root = "/testLock";
public static String path = "/lock";
private static ZooKeeper zk;
private static final String address = "zk01:2181,zk02:2181,zk03:2181,zk04:2181" + root;
private static final CountDownLatch latch = new CountDownLatch(1);
public static ZooKeeper getZk() {
try {
zk = new ZooKeeper(address, 3000, new DefaultWatcher(latch));
latch.await();
} catch (Exception e) {
e.printStackTrace();
}
return zk;
}
}
ZkUtils 作为一个工具类,主要是通过阻塞的方式获取 zookeeper 连接的实例。
address是zookeeper集群的连接地址,指定了本实验的根目录 /testLock,锁节点名称前缀 /lock。
DefaultWatcher是默认的监听类,当连接成功时把 CountDownLatch 减1,返回zk对象。
DefaultWatcher类
package org.example.lock;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import java.util.concurrent.CountDownLatch;
public class DefaultWatcher implements Watcher {
private CountDownLatch latch;
public DefaultWatcher(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void process(W