浅谈分布式锁--基于Zookeeper实现篇:
1、基于zookeeper临时有序节点可以实现的分布式锁。其实基于ZooKeeper,就是使用它的临时有序节点来实现的分布式锁。
来看下Zookeeper能不能解决前面提到的问题。
锁无法释放:使用Zookeeper可以有效的解决锁无法释放的问题,因为在创建锁的时候,客户端会在ZK中创建一个临时节点,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除掉。其他客户端就可以再次获得锁。
非阻塞锁:使用Zookeeper可以实现阻塞的锁,客户端可以通过在ZK中创建顺序节点,并且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户端可以检查自己创建的节点是不是当前所有节点中序号最小的,如果是,那么自己就获取到锁,便可以执行业务逻辑了。
不可重入:使用Zookeeper也可以有效的解决不可重入的问题,客户端在创建节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就可以了。如果和自己的信息一样,那么自己直接获取到锁,如果不一样就再创建一个临时的顺序节点,参与排队。
单点问题:使用Zookeeper可以有效的解决单点问题,ZK是集群部署的,只要集群中有半数以上的机器存活,就可以对外提供服务。
2、实现原理:
原理就是:当某客户端要进行逻辑的加锁时,就在zookeeper上的某个指定节点的目录下,去生成一个唯一的临时有序节点, 然后判断自己是否是这些有序节点中序号最小的一个,如果是,则算是获取了锁。如果不是,则说明没有获取到锁,那么就需要在序列中找到比自己小的那个节点,并对其调用exist()方法,对其注册事件监听,当监听到这个节点被删除了,那就再去判断一次自己当初创建的节点是否变成了序列中最小的。如果是,则获取锁,如果不是,则重复上述步骤。
如图,locker是一个持久节点,node_1/node_2/…/node_n 就是上面说的临时节点,由客户端client去创建的。
client_1/client_2/…/clien_n 都是想去获取锁的客户端。以client_1为例,它想去获取分布式锁,则需要跑到locker下面去创建临时节点(假如是node_1)创建完毕后,
看一下自己的节点序号是否是locker下面最小的,如果是,则获取了锁。如果不是,则去找到比自己小的那个节点(假如是node_2),找到后,就监听node_2,直到node_2被删除,
那么就开始再次判断自己的node_1是不是序列中最小的,如果是,则获取锁,如果还不是,则继续找一下一个节点。
3、优点
锁安全性高,zk可持久化
4、缺点
性能开销比较高。因为其需要动态产生、销毁瞬时节点来实现锁功能。
5、实现
可以直接采用zookeeper第三方库curator即可方便地实现分布式锁
6、zookeeper还有几个特质,让它非常适合作为分布式锁服务。
zookeeper支持watcher机制,这样实现阻塞锁,可以watch锁数据,等到数据被删除,zookeeper会通知客户端去重新竞争锁。
zookeeper的数据可以支持临时节点的概念,即客户端写入的数据是临时数据,在客户端宕机后,临时数据会被删除,这样就实现了锁的异常释放。使用这样的方式,就不需要给锁增加超时自动释放的特性了。
zookeeper实现锁的方式是客户端一起竞争写某条数据,比如/path/lock,只有第一个客户端能写入成功,其他的客户端都会写入失败。
写入成功的客户端就获得了锁,写入失败的客户端,注册watch事件,等待锁的释放,从而继续竞争该锁。
7、节点介绍:
1、PERSISTENT-持久化目录节点
客户端与Zookeeper断开连接后,该节点依旧存在
2、PERSISTENT_SEQUENTIAL-持久化顺序编号目录节点
客户端与Zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号
3、EPHEMERAL-临时目录节点
客户端与Zookeeper断开连接后,该节点被删除
4、EPHEMERAL_SEQUENTIAL-临时顺序编号目录节点
客户端与Zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号
监视器(watcher):
当创建一个节点时,可以注册一个该节点的监视器,当节点状态发生改变时,watch被触发时,ZooKeeper将会向客户端发送且仅发送一条通知,因为watch只能被触发一次。
8、如何利用这些特性来实现分布式锁:
1. 创建一个锁目录lock。
2. 希望获得锁的线程A就在lock目录下,创建临时顺序节点。
3. 获取锁目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁。
4. 线程B获取所有节点,判断自己不是最小节点,设置监听(watcher)比自己次小的节点(只关注比自己次小的节点是为了防止发生“羊群效应”)。
5. 线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是最小的节点,获得锁。
9、实例实现:
public class ZKLock implements Watcher {
private int threadId;
private ZooKeeper zk = null;
private String selfPath;
private String waitPath;
private String CURRENT_THREAD;
private static final String GROUP_PATH = "/disLocks";
private static final String SUB_PATH = "/disLocks/sub";
private static final String CONNECTION_STRING = "192.168.10.41:2181";
private static final int THREAD_NUM = 10;
// 确保连接zk成功;
private CountDownLatch connectedSemaphore = new CountDownLatch(1);
// 确保所有线程运行结束;
private static final CountDownLatch threadSemaphore = new CountDownLatch(THREAD_NUM);
private static final Logger LOG = LoggerFactory.getLogger(ZKLock.class);
public ZKLock(int id) {
this.threadId = id;
CURRENT_THREAD = "【第" + threadId + "个线程】";
}
public static void main(String[] args) {
for (int i = 0; i < THREAD_NUM; i++) {
final int threadId = i + 1;
new Thread() {
@Override
public void run() {
try {
ZKLock dc = new ZKLock(threadId);
dc.createZKConnection(CONNECTION_STRING, 1000);
// GROUP_PATH不存在的话,由一个线程创建即可;
synchronized (threadSemaphore) {
dc.createPath(GROUP_PATH, "该节点由线程" + threadId + "创建", true);
}
dc.getLock();
} catch (Exception e) {
LOG.error("【第" + threadId + "个线程】 抛出的异常:");
e.printStackTrace();
}
}
}.start();
}
try {
threadSemaphore.await();
LOG.info("所有线程运行结束!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 创建节点
*
* @param path
* 节点path
* @param data
* 初始数据内容
* @return
*/
public boolean createPath(String path, String data, boolean needWatch)
throws KeeperException, InterruptedException {
if (zk.exists(path, needWatch) == null) {
LOG.info(CURRENT_THREAD + "节点创建成功, Path: "
+ this.zk.create(path, data.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT)
+ ", content: " + data);
}
return true;
}
/**
* 创建ZK连接
*/
public void createZKConnection(String connectString, int sessionTimeout) throws IOException, InterruptedException {
zk = new ZooKeeper(connectString, sessionTimeout, this);
connectedSemaphore.await();
}
/**
* 获取锁
*/
private void getLock() throws KeeperException, InterruptedException {
try {
selfPath = zk.create(SUB_PATH, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
} catch (KeeperException e) {
e.printStackTrace();
}
LOG.info(CURRENT_THREAD + "创建锁路径:" + selfPath);
if (checkMinPath()) {
// 如果是最小节点获得锁
getLockSuccess();
}
}
/**
* 获取锁成功
*/
public void getLockSuccess() throws KeeperException, InterruptedException {
if (zk.exists(this.selfPath, false) == null) {
LOG.error(CURRENT_THREAD + "本节点已不在了...");
return;
}
LOG.info(CURRENT_THREAD + "获取锁成功!");
// 处理业务逻辑
Thread.sleep(2000);
unlock();
}
/**
* 释放锁
*/
public void unlock() {
LOG.info(CURRENT_THREAD + "删除本节点:" + selfPath);
try {
zk.delete(this.selfPath, -1);
releaseConnection();
threadSemaphore.countDown();
System.out.println(CURRENT_THREAD + "节点的锁已经释放了!");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
/**
* 关闭ZK连接
*/
public void releaseConnection() {
if (this.zk != null) {
try {
this.zk.close();
} catch (InterruptedException e) {
}
}
LOG.info(CURRENT_THREAD + "释放连接");
}
/**
* 检查自己是不是最小的节点
*
* @return
*/
public boolean checkMinPath() throws KeeperException, InterruptedException {
List<String> subNodes = zk.getChildren(GROUP_PATH, false);
Collections.sort(subNodes);
int index = subNodes.indexOf(selfPath.substring(GROUP_PATH.length() + 1));
switch (index) {
case -1: {
LOG.error(CURRENT_THREAD + "节点已不在了..." + selfPath);
return false;
}
case 0: {
LOG.info(CURRENT_THREAD + "节点可以获得锁了" + selfPath);
return true;
}
default: {
this.waitPath = GROUP_PATH + "/" + subNodes.get(index - 1);
LOG.info(CURRENT_THREAD + "获取子节点中,排在我前面的" + waitPath);
try {
zk.getData(waitPath, true, new Stat());
return false;
} catch (KeeperException e) {
if (zk.exists(waitPath, false) == null) {
LOG.info(CURRENT_THREAD + "子节点中,排在前面的" + waitPath + "已丢失");
return checkMinPath();
} else {
throw e;
}
}
}
}
}
@Override
public void process(WatchedEvent event) {
if (event == null) {
return;
}
Event.KeeperState keeperState = event.getState();
Event.EventType eventType = event.getType();
if (Event.KeeperState.SyncConnected == keeperState) {
if (Event.EventType.None == eventType) {
LOG.info(CURRENT_THREAD + "成功连接ZK服务器");
connectedSemaphore.countDown();
} else if (event.getType() == Event.EventType.NodeDeleted && event.getPath().equals(waitPath)) {
LOG.info(CURRENT_THREAD + "排前面的节点丢失,判断该节点是否可以获得锁?");
try {
if (checkMinPath()) {
getLockSuccess();
}
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} else if (Event.KeeperState.Disconnected == keeperState) {
LOG.info(CURRENT_THREAD + "与ZK服务器断开连接");
} else if (Event.KeeperState.AuthFailed == keeperState) {
LOG.info(CURRENT_THREAD + "权限检查失败");
} else if (Event.KeeperState.Expired == keeperState) {
LOG.info(CURRENT_THREAD + "会话失效");
}
}
}
10、总结:(建议采用的方式)
使用Zookeeper实现分布式锁的优点
有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题。实现起来较为简单。
使用Zookeeper实现分布式锁的缺点
性能上不如使用缓存实现分布式锁。 需要对ZK的原理有所了解。