关于ZooKeeper,上一篇博客有更详细的介绍(传送门)。
在同一个jvm进程中时,可以使用JUC提供的一些锁来解决多个线程竞争同一个共享资源时候的线程安全问题,但是当多个不同机器上的不同jvm进程共同竞争同一个共享资源时候,juc包的锁就无能无力了,这时候就需要分布式锁了。
常见的分布式锁实现方式有Redis的SETNX和GETSET函数,数据库锁,zk的数据节点和监听事件等。
其中Redis版本的实现之前已有博客介绍(传送门),现在就谈一下使用ZooKeeper实现的方案。
在zk中是使用文件目录的格式(或者叫树形结构)存放节点内容,其中节点类型分为:
具体在节点创建过程中,可以混合使用,比如临时顺序节点(EPHEMERAL_SEQUENTIAL)。
由此,我们可以得到两种实现方案。
这种方式的特点是,线程获得锁的先后顺序跟创建节点的先后顺序保持了一致,所以我称为公平模式。还有就是,不存在锁释放之后多线程争抢的问题,性能会更好。
这种方式的特点是,当第一个获得锁的线程释放锁之后,其它在等待的所有线程会一起去争抢这把锁,不存在固定的先后顺序。
org.apache.zookeeper
zookeeper
3.4.13
因为同时提供公平和非公平两种方式的实现,所以在类中定义了两个子内部类,各自实现不同的lock和unlock方法。外层类提供了根据参数创建不同子类对象的静态方法。
另一个内部类LockWatcher的设计也很重要:构造函数传入初始值为1的CountDownLatch,监听NodeDelete事件,被触发时倒计数锁存器减一让线程由等待状态继续往下执行。
/**
* zk分布式锁
* @author z_hh
* @time 2018年12月31日
*/
public class ZookeeperDistributedLock {
// zk客户端
private ZooKeeper zk;
// zk是一个目录结构,root为最外层目录
private String root = "/locks";
// 用来同步等待zkclient链接到了服务端
private CountDownLatch connectedSignal = new CountDownLatch(1);
// 会话超时时间:毫秒
private final static int sessionTimeout = 3000;
// 节点数据:无需数据
private final static byte[] data= new byte[0];
/**
* 创建一个zk分布式锁实例
* @param config zk连接字符串
* @param lockName 锁名称
* @param isFair 是否公平
* @return 公平 or 非公平锁对象
*/
public static ZookeeperDistributedLock create(String config, String lockName, boolean isFair) {
return isFair ? new ZookeeperDistributedLock(config, lockName).new FairLock(config, lockName)
: new ZookeeperDistributedLock(config, lockName).new UnFairLock(config, lockName);
}
// 构造函数私有化
private ZookeeperDistributedLock(String config, String lockName) {
try {
zk = new ZooKeeper(config, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent event) {
// 建立连接
if (event.getState() == KeeperState.SyncConnected) {
connectedSignal.countDown();
}
}
});
// 等待连接建立完毕
connectedSignal.await();
Stat stat = zk.exists(root, false);
if (null == stat) {
// 创建根节点
zk.create(root, data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 锁
* @throws Exception
*/
public void lock() throws Exception {
// 具体子类实现
throw new UnsupportedOperationException("不支持的操作!");
}
/**
* 解锁
*/
public void unlock() {
// 具体子类实现
throw new UnsupportedOperationException("不支持的操作!");
}
// 监听器:一旦锁被释放,从等待状态唤醒继续往下执行
private class LockWatcher implements Watcher {
private CountDownLatch latch = null;
public LockWatcher(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void process(WatchedEvent event) {
if (event.getType() == Event.EventType.NodeDeleted)
latch.countDown();
}
}
// 公平锁实现
private class FairLock extends ZookeeperDistributedLock {
//锁的名称
private String lockName;
//当前线程创建的序列node
private ThreadLocal nodeId = new ThreadLocal<>();
public FairLock(String config, String lockName) {
super(config, lockName);
this.lockName = lockName;
}
@Override
public void lock() throws Exception {
try {
// 创建临时子节点,含序列
String myNode = zk.create(root + "/" + lockName , data, ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(Thread.currentThread().getName() + myNode + "created");
// 取出所有子节点
List subNodes = zk.getChildren(root, false);
TreeSet sortedNodes = new TreeSet<>();// 字典序
for(String node :subNodes) {
sortedNodes.add(root +"/" +node);
}
String smallNode = sortedNodes.first();// 最小,即第一个创建的
String preNode = sortedNodes.lower(myNode);// 前一个
if (myNode.equals( smallNode)) {
// 如果是最小的节点,则表示取得锁
System.out.println(Thread.currentThread().getName() + myNode + "get lock");
this.nodeId.set(myNode);
return;
}
CountDownLatch latch = new CountDownLatch(1);
Stat stat = zk.exists(preNode, new LockWatcher(latch));// 同时注册监听。
// 判断比自己小一个数的节点是否存在,如果存在等待锁,同时注册监听
if (stat != null) {
System.out.println(Thread.currentThread().getName() + myNode +
" waiting for " + root + "/" + preNode + " released lock");
latch.await();// 等待,这里应该一直等待其他线程释放锁
nodeId.set(myNode);
latch = null;
}
// 不存在,说明锁释放了,轮到自己了
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void unlock() {
try {
System.out.println(Thread.currentThread().getName() + nodeId.get() + "unlock ");
if (null != nodeId) {
zk.delete(nodeId.get(), -1);// 删除节点
}
nodeId.remove();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
}
// 非公平锁实现
private class UnFairLock extends ZookeeperDistributedLock {
// 锁的全路径
private String lockPath;
public UnFairLock(String config, String lockName) {
super(config, lockName);
lockPath = root + "/" + Objects.requireNonNull(lockName, "lockName不能为null!");
}
@Override
public void lock() throws Exception {
while (true) {
try {
zk.create(lockPath , data, ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL);
System.out.println(Thread.currentThread().getName() + "get lock");
return;
} catch (NodeExistsException e) {// 说明锁被占用,注册监听并等待
CountDownLatch latch = new CountDownLatch(1);
Stat stat = zk.exists(lockPath, new LockWatcher(latch));// 注册监听。
if (stat != null) {
System.out.println(Thread.currentThread().getName() +
" waiting for " + lockPath + " released lock");
latch.await();// 等待
latch = null;
}
} catch (Exception e) {
throw e;
}
}
}
@Override
public void unlock() {
try {
System.out.println(Thread.currentThread().getName() + "unlock ");
zk.delete(lockPath, -1);// 删除节点
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
}
}
分别测试了两种方式的分布式锁。代码中使用CountDownLatch、Thread.sleep、parallelStream尽量模拟了真实场景。
public class ZkLockTest {
// zk地址
private String CONNECT_STRING = "xxoo:2181";
// 公平
@Test
public void testFair() throws Throwable {
test(true);
}
// 非公平
@Test
public void testUnFair() throws Throwable {
test(false);
}
public void test(boolean isFair) throws Exception {
List threads = new ArrayList<>();
// 线程数
int threadCount = 10;
// 等待所有线程执行完毕
CountDownLatch latch = new CountDownLatch(threadCount);
// 准备线程及执行任务
for (int i = 0; i < threadCount; i++) {
threads.add(new Thread(() -> {
ZookeeperDistributedLock zkLock = ZookeeperDistributedLock.create(CONNECT_STRING, "zhh_lock", isFair);
try {
// 抢锁
zkLock.lock();
// 模拟做事
doSomething(1);
// 解锁
zkLock.unlock();
latch.countDown();
} catch (Exception e) {
e.printStackTrace();
}
}, "thread-" + i));
}
// 并行
threads.parallelStream()
.forEach(Thread::start);
// 等待所有线程执行完毕
latch.await();
}
// 模拟做一些事情
private void doSomething(int second) {
try {
System.out.println(Thread.currentThread().getName() + "获得了锁,开始执行任务!");
Thread.sleep(second * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
第一种,公平方式。
第二种,非公平方式。