分布式锁
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,这个时候,便需要使用到分布式锁。
定义一个Lock接口,需要实现获取锁和释放锁。
public interface Lock {
/**
* 获取锁
*/
public void getLock();
/**
* 释放锁
*/
public void unLock();
}
ZooKeeper实现分布式锁,主要得益于ZooKeeper保证了数据的强一致性这一特性。ZK实现分布式锁主要有两个实现,一个是基于同名节点,另一个是基于不同名节点。
ZookeeperAbstractLock作为抽象类,定义一个获取锁的流程模板。
public abstract class ZookeeperAbstractLock implements Lock {
private static final String CONNECTION = "127.0.0.1:2181";
protected ZkClient zkClient = new ZkClient(CONNECTION);
public void getLock() {
// 尝试获取锁
if (tryLock()) {
System.out.println("获取分布式锁!");
} else {
// 等待
waitLock();
// 重新获取资源
getLock();
}
}
public abstract boolean tryLock();
public abstract void waitLock();
}
基于同名节点的实现
监听同一个节点,创建成功则获取到锁。通过一个CountDownLatch控制计数器,其他线程实现等待唤醒。这种方式存在锁的竞争,当请求线程数很多时,可能会影响性能。
public class ZookeeperDistributeLock extends ZookeeperAbstractLock {
protected static final String PATH = "/lock";
private CountDownLatch countDownLatch = null;
@Override
public boolean tryLock() {
try {
zkClient.createEphemeral(PATH);
return true;
} catch (Exception e) {
return false;
}
}
@Override
public void waitLock() {
// 监听事件
IZkDataListener iZkDataListener = new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {
// 唤醒等待的线程
if (countDownLatch != null) {
countDownLatch.countDown();
}
}
@Override
public void handleDataDeleted(String s) throws Exception {
}
};
// 注册监听事件
zkClient.subscribeDataChanges(PATH, iZkDataListener);
if (zkClient.exists(PATH)) {
countDownLatch= new CountDownLatch(1);
try {
// 等待事件通知
countDownLatch.await();
} catch (Exception e) {
e.printStackTrace();
}
}
// 删除监听事件
zkClient.unsubscribeDataChanges(PATH, iZkDataListener);
}
@Override
public void unLock() {
if (zkClient != null) {
zkClient.delete(PATH);
zkClient.close();
System.out.println("释放锁!");
}
}
}
基于不同名节点的实现
通过一个顺序节点,每个线程请求时获取各自编号,如果是第一个节点则获取锁成功,否则并且获取前一个节点编号,并监听前一个节点变化。当前个节点变化后,通过CountDownLatch唤醒。这种方式更类似队列,减少了锁的竞争开销,每个线程排队拿锁。
public class ZookeeperDistributeLock2 extends ZookeeperAbstractLock {
protected static final String PATH = "/lock2";
private CountDownLatch countDownLatch = null;
private String beforePath; // 当前请求节点的前一个节点
private String currentPath; // 当前节点
public ZookeeperDistributeLock2() {
if (!this.zkClient.exists(PATH)) {
this.zkClient.createPersistent(PATH);
}
}
@Override
public boolean tryLock() {
// 如果currentPath为空,则为第一次尝试加锁,赋值currentPath
if (currentPath == null || currentPath.length() <= 0) {
currentPath = this.zkClient.createEphemeralSequential(PATH + "/", "lock");
}
// 获取所有临时节点并排序,临时节点名称为自增长的字符串
List<String> children = this.zkClient.getChildren(PATH);
Collections.sort(children);
if (currentPath.equals(PATH + "/" + children.get(0))) {
// 如果当前节点在所有节点中排第一,则获取锁成功
return true;
} else {
// 如果当前节点不在所有节点中排第一,则获取前面节点名称,并赋值给beforePath
int wz = Collections.binarySearch(children, currentPath.substring(7));
beforePath = PATH + "/" + children.get(wz - 1);
}
return false;
}
@Override
public void waitLock() {
IZkDataListener listener = new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {
if (countDownLatch != null) {
countDownLatch.countDown();
}
}
@Override
public void handleDataDeleted(String s) throws Exception {
}
};
// 给排在前面的节点增加数据删除的watcher,本质是启动另一个线程去监听前置节点
this.zkClient.subscribeDataChanges(beforePath, listener);
if (this.zkClient.exists(beforePath)) {
countDownLatch = new CountDownLatch(1);
try {
countDownLatch.await();
} catch (Exception e) {
e.printStackTrace();
}
}
this.zkClient.unsubscribeDataChanges(beforePath, listener);
}
@Override
public void unLock() {
this.zkClient.delete(currentPath);
this.zkClient.close();
}
}