[转载]作者和原文链接,?
?
场景描述
在分布式应用, 往往存在多个进程提供同一服务. 这些进程有可能在相同的机器上, 也有可能分布在不同的机器上. 如果这些进程共享了一些资源, 可能就需要分布式锁来锁定对这些资源的访问.
本文将介绍如何利用zookeeper实现分布式锁.
思路
进程需要访问共享数据时, 就在"/locks"节点下创建一个sequence类型的子节点, 称为thisPath. 当thisPath在所有子节点中最小时, 说明该进程获得了锁. 进程获得锁之后, 就可以访问共享资源了. 访问完成后, 需要将thisPath删除. 锁由新的最小的子节点获得.
有了清晰的思路之后, 还需要补充一些细节. 进程如何知道thisPath是所有子节点中最小的呢? 可以在创建的时候, 通过getChildren方法获取子节点列表, 然后在列表中找到排名比thisPath前1位的节点, 称为waitPath, 然后在waitPath上注册监听, 当waitPath被删除后, 进程获得通知, 此时说明该进程获得了锁.
实现
以一个DistributedClient对象模拟一个进程的形式, 演示zookeeper分布式锁的实现.
Java代码
class="Apple-converted-space">??
- public?class?DistributedClient?{??
- ??????
- ????private?static?final?int?SESSION_TIMEOUT?=?5000;??
- ??????
- ????private?String?hosts?=?"localhost:4180,localhost:4181,localhost:4182";??
- ????private?String?groupNode?=?"locks";??
- ????private?String?subNode?=?"sub";??
- ??
- ????private?ZooKeeper?zk;??
- ??????
- ????private?String?thisPath;??
- ??????
- ????private?String?waitPath;??
- ??
- ????private?CountDownLatch?latch?=?new?CountDownLatch(1);??
- ??
- ?????
- ?
- ??
- ????public?void?connectZookeeper()?throws?Exception?{??
- ????????zk?=?new?ZooKeeper(hosts,?SESSION_TIMEOUT,?new?Watcher()?{??
- ????????????public?void?process(WatchedEvent?event)?{??
- ????????????????try?{??
- ??????????????????????
- ????????????????????if?(event.getState()?==?KeeperState.SyncConnected)?{??
- ????????????????????????latch.countDown();??
- ????????????????????}??
- ??
- ??????????????????????
- ????????????????????if?(event.getType()?==?EventType.NodeDeleted?&&?event.getPath().equals(waitPath))?{??
- ????????????????????????doSomething();??
- ????????????????????}??
- ????????????????}?catch?(Exception?e)?{??
- ????????????????????e.printStackTrace();??
- ????????????????}??
- ????????????}??
- ????????});??
- ??
- ??????????
- ????????latch.await();??
- ??
- ??????????
- ????????thisPath?=?zk.create("/"?+?groupNode?+?"/"?+?subNode,?null,?Ids.OPEN_ACL_UNSAFE,??
- ????????????????CreateMode.EPHEMERAL_SEQUENTIAL);??
- ??
- ??????????
- ????????Thread.sleep(10);??
- ??
- ??????????
- ????????List<String>?childrenNodes?=?zk.getChildren("/"?+?groupNode,?false);??
- ??
- ??????????
- ????????if?(childrenNodes.size()?==?1)?{??
- ????????????doSomething();??
- ????????}?else?{??
- ????????????String?thisNode?=?thisPath.substring(("/"?+?groupNode?+?"/").length());??
- ??????????????
- ????????????Collections.sort(childrenNodes);??
- ????????????int?index?=?childrenNodes.indexOf(thisNode);??
- ????????????if?(index?==?-1)?{??
- ??????????????????
- ????????????}?else?if?(index?==?0)?{??
- ??????????????????
- ????????????????doSomething();??
- ????????????}?else?{??
- ??????????????????
- ????????????????this.waitPath?=?"/"?+?groupNode?+?"/"?+?childrenNodes.get(index?-?1);??
- ??????????????????
- ????????????????zk.getData(waitPath,?true,?new?Stat());??
- ????????????}??
- ????????}??
- ????}??
- ??
- ????private?void?doSomething()?throws?Exception?{??
- ????????try?{??
- ????????????System.out.println("gain?lock:?"?+?thisPath);??
- ????????????Thread.sleep(2000);??
- ??????????????
- ????????}?finally?{??
- ????????????System.out.println("finished:?"?+?thisPath);??
- ??????????????
- ??????????????
- ????????????zk.delete(this.thisPath,?-1);??
- ????????}??
- ????}??
- ??
- ????public?static?void?main(String[]?args)?throws?Exception?{??
- ????????for?(int?i?=?0;?i?<?10;?i++)?{??
- ????????????new?Thread()?{??
- ????????????????public?void?run()?{??
- ????????????????????try?{??
- ????????????????????????DistributedClient?dl?=?new?DistributedClient();??
- ????????????????????????dl.connectZookeeper();??
- ????????????????????}?catch?(Exception?e)?{??
- ????????????????????????e.printStackTrace();??
- ????????????????????}??
- ????????????????}??
- ????????????}.start();??
- ????????}??
- ??
- ????????Thread.sleep(Long.MAX_VALUE);??
- ????}??
- }???
思考
思维缜密的朋友可能会想到, 上述的方案并不安全. 假设某个client在获得锁之前挂掉了, 由于client创建的节点是ephemeral类型的, 因此这个节点也会被删除, 从而导致排在这个client之后的client提前获得了锁. 此时会存在多个client同时访问共享资源.
如何解决这个问题呢? 可以在接到waitPath的删除通知的时候, 进行一次确认, 确认当前的thisPath是否真的是列表中最小的节点.
Java代码
??
- ??
- if?(event.getType()?==?EventType.NodeDeleted?&&?event.getPath().equals(waitPath))?{??
- ??????
- ????List<String>?childrenNodes?=?zk.getChildren("/"?+?groupNode,?false);??
- ????String?thisNode?=?thisPath.substring(("/"?+?groupNode?+?"/").length());??
- ??????
- ????Collections.sort(childrenNodes);??
- ????int?index?=?childrenNodes.indexOf(thisNode);??
- ????if?(index?==?0)?{??
- ??????????
- ????????doSomething();??
- ????}?else?{??
- ??????????
- ??????????
- ????????waitPath?=?"/"?+?groupNode?+?"/"?+?childrenNodes.get(index?-?1);??
- ??????????
- ????????if?(zk.exists(waitPath,?true)?==?null)?{??
- ????????????doSomething();??
- ????????}??
- ????}??
- }??
另外, 由于thisPath和waitPath这2个成员变量会在多个线程中访问, 最好将他们声明为volatile, 以防止出现线程可见性问题.
另一种思路
下面介绍一种更简单, 但是不怎么推荐的解决方案.
每个client在getChildren的时候, 注册监听子节点的变化. 当子节点的变化通知到来时, 再一次通过getChildren获取子节点列表, 判断thisPath是否是列表中的最小节点, 如果是, 则执行资源访问逻辑.
Java代码
??
- public?class?DistributedClient2?{??
- ??????
- ????private?static?final?int?SESSION_TIMEOUT?=?5000;??
- ??????
- ????private?String?hosts?=?"localhost:4180,localhost:4181,localhost:4182";??
- ????private?String?groupNode?=?"locks";??
- ????private?String?subNode?=?"sub";??
- ??
- ????private?ZooKeeper?zk;??
- ??????
- ????private?volatile?String?thisPath;??
- ??
- ????private?CountDownLatch?latch?=?new?CountDownLatch(1);??
- ??
- ?????
- ?
- ??
- ????public?void?connectZookeeper()?throws?Exception?{??
- ????????zk?=?new?ZooKeeper(hosts,?SESSION_TIMEOUT,?new?Watcher()?{??
- ????????????public?void?process(WatchedEvent?event)?{??
- ????????????????try?{??
- ??????????????????????
- ????????????????????if?(event.getState()?==?KeeperState.SyncConnected)?{??
- ????????????????????????latch.countDown();??
- ????????????????????}??
- ??
- ??????????????????????
- ????????????????????if?(event.getType()?==?EventType.NodeChildrenChanged?&&?event.getPath().equals("/"?+?groupNode))?{??
- ??????????????????????????
- ????????????????????????List<String>?childrenNodes?=?zk.getChildren("/"?+?groupNode,?true);??
- ????????????????????????String?thisNode?=?thisPath.substring(("/"?+?groupNode?+?"/").length());??
- ??????????????????????????
- ????????????????????????Collections.sort(childrenNodes);??
- ????????????????????????if?(childrenNodes.indexOf(thisNode)?==?0)?{??
- ????????????????????????????doSomething();??
- ????????????????????????}??
- ????????????????????}??
- ????????????????}?catch?(Exception?e)?{??
- ????????????????????e.printStackTrace();??
- ????????????????}??
- ????????????}??
- ????????});??
- ??
- ??????????
- ????????latch.await();??
- ??
- ??????????
- ????????thisPath?=?zk.create("/"?+?groupNode?+?"/"?+?subNode,?null,?Ids.OPEN_ACL_UNSAFE,??
- ????????????????CreateMode.EPHEMERAL_SEQUENTIAL);??
- ??
- ??????????
- ????????Thread.sleep(10);??
- ??
- ??????????
- ????????List<String>?childrenNodes?=?zk.getChildren("/"?+?groupNode,?true);??
- ??
- ??????????
- ????????if?(childrenNodes.size()?==?1)?{??
- ????????????doSomething();??
- ????????}??
- ????}??
- ??
- ?????
- ?
- ??
- ????private?void?doSomething()?throws?Exception?{??
- ????????try?{??
- ????????????System.out.println("gain?lock:?"?+?thisPath);??
- ????????????Thread.sleep(2000);??
- ??????????????
- ????????}?finally?{??
- ????????????System.out.println("finished:?"?+?thisPath);??
- ??????????????
- ??????????????
- ????????????zk.delete(this.thisPath,?-1);??
- ????????}??
- ????}??
- ??
- ????public?static?void?main(String[]?args)?throws?Exception?{??
- ????????for?(int?i?=?0;?i?<?10;?i++)?{??
- ????????????new?Thread()?{??
- ????????????????public?void?run()?{??
- ????????????????????try?{??
- ????????????????????????DistributedClient2?dl?=?new?DistributedClient2();??
- ????????????????????????dl.connectZookeeper();??
- ????????????????????}?catch?(Exception?e)?{??
- ????????????????????????e.printStackTrace();??
- ????????????????????}??
- ????????????????}??
- ????????????}.start();??
- ????????}??
- ??
- ????????Thread.sleep(Long.MAX_VALUE);??
- ????}??
- }??
为什么不推荐这个方案呢? 是因为每次子节点的增加和删除都要广播给所有client, client数量不多时还看不出问题. 如果存在很多client, 那么就可能导致广播风暴--过多的广播通知阻塞了网络. 使用第一个方案, 会使得通知的数量大大下降. 当然第一个方案更复杂一些, 复杂的方案同时也意味着更容易引进bug.