#使用Zookeeper实现分布式锁
目前,分布式应用渐渐取代了传统的单体应用,也有越来越多的程序员投入到分布式应用开发中。既然是分布式开发,肯定会遇到共享资源的访问(修改)问题。单体应用中多线程访问(修改)共享资源,我们想到的解决方案是synchronized、ReentrantLock,以保证数据的安全性和一致性。那么问题来了,分布式应用如何实现分布式锁呢?
好慌,打开浏览器百度一波,我们得到的答案如下:
开心,原来分布式锁的实现已经有了成熟的解决方案。那么,我们需要做的就是用代码去实现这些方案。笔主在项目开发过程中,也遇到过共享资源的访问(修改)问题。当时为了快速解决这个问题,选用了第二种方案,基于Redis实现分布式锁。为什么选用了Redis?理由如下:
闲下来的时候,还是想用不同的方案去解决同一个问题。那么本篇文章的主题来了,那就是使用ZooKeeper实现分布式锁。
这里直接借用百度百科对ZooKeeper下的定义。
ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。ZooKeeper的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
简单理解,ZooKeeper将复杂的服务封装为简单的API,ZooKeeper集群保证了其可以提供稳定的服务。
那么,如何基于ZooKeeper实现分布式锁呢?思路如下:
这个实现思路主要依赖了ZooKeeper的哪些性质呢?
package org.apache.zookeeper;
/***
2. CreateMode value determines how the znode is created on ZooKeeper.
*/
public enum CreateMode {
PERSISTENT (0, false, false),
PERSISTENT_SEQUENTIAL (2, false, true),
EPHEMERAL (1, true, false),
EPHEMERAL_SEQUENTIAL (3, true, true);
}
2.ZooKeeper的事件监听特性。当ZooKeeper客户端访问节点时,可以对节点设置事件监听,当节点创建、删除、数据变化、子节点变化时,会通知监听的客户端。
/**
1. Enumeration of types of events that may occur on the ZooKeeper
*/
public enum EventType {
None (-1),
NodeCreated (1),
NodeDeleted (2),
NodeDataChanged (3),
NodeChildrenChanged (4);
}
吧啦吧啦讲了这么多,现在上具体的实现代码。
package com.example.demo1.distribute.lock;
import java.io.IOException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
public class ZKClient {
private static ZooKeeper instance = null;
//连接地址
private final static String CONNECT_STRING ="127.0.0.1:2181";
//会话超时时间
private final static int SESSION_TIMEOUT = 3000;
//私有的构造方法
private ZKClient () {}
/**
* 获取连接实例
* @return
*/
public static ZooKeeper getInstance() {
if (instance == null) {
synchronized (ZKClient.class) {
if (instance == null) {
try {
instance = new ZooKeeper(CONNECT_STRING, SESSION_TIMEOUT, new Watcher(){
@Override
public void process(WatchedEvent event) {
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
return instance;
}
}
ZKClient这个类便于获取Zookeeper的连接实例,也就是客户端。使用单例模式实现,在分布式应用中,每一个应用使用一个客户端和ZooKeeper服务交互即可。
package com.example.demo1.distribute.lock;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.EventType;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* ZK实现分布式锁
* @author zhaoheng
* @date 2018年8月3日
*/
public class DistributeLock {
private static final Logger LOG = LoggerFactory.getLogger(DistributeLock.class);
//根节点
private static final String ROOT_NODE_LOCK = "/ROOT_LOCK";
private ZooKeeper zooKeeper;
//当前节点锁的id
private String currentLockId;
//节点存放的数据
private final static String DATA = "node";
private final CountDownLatch countDownLatch = new CountDownLatch(1);
public DistributeLock () {
//初始化连接实例
this.zooKeeper = ZKClient.getInstance();
}
/**
* 检查根节点
*/
public void checkRootNode () {
try {
//先判断根节点是否存在
Stat stat = zooKeeper.exists(ROOT_NODE_LOCK, false);
if (stat == null) {
//根节点不存在则创建根节点
zooKeeper.create(ROOT_NODE_LOCK, DATA.getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
} catch (Exception e) {
LOG.error("创建根节点出现异常:", e);
}
}
/***
* 获取分布式锁
* @return
*/
public synchronized boolean lock () {
try {
//在根节点下创建临时顺序节点
currentLockId = zooKeeper.create(ROOT_NODE_LOCK + "/", DATA.getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
LOG.info("线程{}创建了节点{}", Thread.currentThread().getName(), currentLockId);
//获取根节点下的子节点集合
List childNodeList = zooKeeper.getChildren(ROOT_NODE_LOCK, true);
//子节点集合排序
Collections.sort(childNodeList);
//获取到第一个节点
String firstNode = childNodeList.get(0);
String currentNode = currentLockId.substring(currentLockId.lastIndexOf("/") + 1);
//如果当前子节点就是第一个子节点
if (currentNode.equals(firstNode)) {
LOG.info("线程{}获取到分布式锁,当前子节点{}", Thread.currentThread().getName(), currentLockId);
return true;
}
int index = childNodeList.indexOf(currentNode);
LOG.info("线程{}对应的子节点不是第一个节点,当前子节点{}", Thread.currentThread().getName(), currentLockId);
if (index > 0) {
//获取当前子节点的前一个节点
String preNode = childNodeList.get(index - 1);
LOG.info("线程{}获取当前子节点的前一个节点,当前子节点{},前一个节点{}", Thread.currentThread().getName() , currentLockId, preNode);
zooKeeper.exists(ROOT_NODE_LOCK + "/" +preNode, new Watcher (){
@Override
public void process(WatchedEvent event) {
//监听前一个节点的删除事件
if (event.getType().equals(EventType.NodeDeleted)) {
LOG.info("当前节点{}监听到前一个节点{}删除事件", currentLockId, preNode);
countDownLatch.countDown();
}
}
});
countDownLatch.await();
LOG.info("线程{}获取到分布式锁,当前子节点{}", Thread.currentThread().getName(), currentLockId);
return true;
}
} catch (Exception e) {
LOG.error("获取分布式锁出现异常:", e);
}
return false;
}
/**
* 释放分布式锁
* @return
*/
public synchronized boolean unlock () {
try {
LOG.info("线程{}开始释放分布式锁,当前子节点{}", Thread.currentThread().getName(), currentLockId);
//删除当前子节点
zooKeeper.delete(currentLockId, -1);
LOG.info("线程{}释放分布式锁成功,当前子节点{}", Thread.currentThread().getName(), currentLockId);
return true;
} catch (Exception e) {
LOG.error("释放分布式锁出现异常:", e);
}
return false;
}
}
DistributeLock类是基于ZooKeeper的具体实现,采用的就是刚才讨论的思路。
/**
* 测试分布式锁
* @author zhaoheng
* @date 2018年8月8日
*/
public class TestDistributeLock {
private static final Logger LOG = LoggerFactory.getLogger(TestDistributeLock.class);
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(10);
//定义一个定长的线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
Random random = new Random();
for (int i = 0; i < 10; i++) {
executorService.execute( new Runnable() {
DistributeLock distributeLock = null;
@Override
public void run() {
try {
distributeLock = new DistributeLock();
//检查根节点
distributeLock.checkRootNode();
countDownLatch.countDown();
countDownLatch.await();
//获取锁
distributeLock.lock();
//随机睡眠一段时间,模拟业务处理
Thread.sleep(random.nextInt(1000));
} catch (Exception e) {
LOG.error("处理业务出现异常:", e);
} finally {
if (distributeLock != null) {
//释放锁
distributeLock.unlock();
}
}
}
});
}
}
}
当然,少不了测试类。在测试类中,使用10个线程模拟竞争分布式锁。另外,也使用了Jdk并发包下的工具类CountDownLatch,10个线程并行执行。
2018-08-08 17:23:56.059 - 线程pool-2-thread-1创建了节点/ROOT_LOCK/0000000000
2018-08-08 17:23:56.091 - 线程pool-2-thread-8创建了节点/ROOT_LOCK/0000000001
2018-08-08 17:23:56.091 - 线程pool-2-thread-10创建了节点/ROOT_LOCK/0000000002
2018-08-08 17:23:56.092 - 线程pool-2-thread-2创建了节点/ROOT_LOCK/0000000003
2018-08-08 17:23:56.092 - 线程pool-2-thread-9创建了节点/ROOT_LOCK/0000000004
2018-08-08 17:23:56.093 - 线程pool-2-thread-4创建了节点/ROOT_LOCK/0000000005
2018-08-08 17:23:56.093 - 线程pool-2-thread-3创建了节点/ROOT_LOCK/0000000006
2018-08-08 17:23:56.093 - 线程pool-2-thread-7创建了节点/ROOT_LOCK/0000000007
2018-08-08 17:23:56.094 - 线程pool-2-thread-6创建了节点/ROOT_LOCK/0000000008
2018-08-08 17:23:56.097 - 线程pool-2-thread-5创建了节点/ROOT_LOCK/0000000009
2018-08-08 17:23:56.101 - 线程pool-2-thread-1获取到分布式锁,当前子节点/ROOT_LOCK/0000000000
2018-08-08 17:23:56.101 - 线程pool-2-thread-8对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000001
2018-08-08 17:23:56.101 - 线程pool-2-thread-8获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000001,前一个节点0000000000
2018-08-08 17:23:56.101 - 线程pool-2-thread-10对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000002
2018-08-08 17:23:56.101 - 线程pool-2-thread-10获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000002,前一个节点0000000001
2018-08-08 17:23:56.102 - 线程pool-2-thread-2对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000003
2018-08-08 17:23:56.103 - 线程pool-2-thread-2获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000003,前一个节点0000000002
2018-08-08 17:23:56.103 - 线程pool-2-thread-3对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000006
2018-08-08 17:23:56.103 - 线程pool-2-thread-3获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000006,前一个节点0000000005
2018-08-08 17:23:56.103 - 线程pool-2-thread-7对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000007
2018-08-08 17:23:56.104 - 线程pool-2-thread-7获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000007,前一个节点0000000006
2018-08-08 17:23:56.103 - 线程pool-2-thread-4对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000005
2018-08-08 17:23:56.105 - 线程pool-2-thread-4获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000005,前一个节点0000000004
2018-08-08 17:23:56.103 - 线程pool-2-thread-9对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000004
2018-08-08 17:23:56.107 - 线程pool-2-thread-9获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000004,前一个节点0000000003
2018-08-08 17:23:56.109 - 线程pool-2-thread-5对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000009
2018-08-08 17:23:56.109 - 线程pool-2-thread-5获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000009,前一个节点0000000008
2018-08-08 17:23:56.109 - 线程pool-2-thread-6对应的子节点不是第一个节点,当前子节点/ROOT_LOCK/0000000008
2018-08-08 17:23:56.112 - 线程pool-2-thread-6获取当前子节点的前一个节点,当前子节点/ROOT_LOCK/0000000008,前一个节点0000000007
2018-08-08 17:23:57.059 - 线程pool-2-thread-1开始释放分布式锁,当前子节点/ROOT_LOCK/0000000000
2018-08-08 17:23:57.237 - 线程pool-2-thread-1释放分布式锁成功,当前子节点/ROOT_LOCK/0000000000
2018-08-08 17:23:57.237 - 当前节点/ROOT_LOCK/0000000001监听到前一个节点0000000000删除事件
2018-08-08 17:23:57.238 - 线程pool-2-thread-8获取到分布式锁,当前子节点/ROOT_LOCK/0000000001
2018-08-08 17:23:57.701 - 线程pool-2-thread-8开始释放分布式锁,当前子节点/ROOT_LOCK/0000000001
2018-08-08 17:23:57.844 - 当前节点/ROOT_LOCK/0000000002监听到前一个节点0000000001删除事件
2018-08-08 17:23:57.844 - 线程pool-2-thread-8释放分布式锁成功,当前子节点/ROOT_LOCK/0000000001
2018-08-08 17:23:57.844 - 线程pool-2-thread-10获取到分布式锁,当前子节点/ROOT_LOCK/0000000002
2018-08-08 17:23:58.032 - 线程pool-2-thread-10开始释放分布式锁,当前子节点/ROOT_LOCK/0000000002
2018-08-08 17:23:58.256 - 当前节点/ROOT_LOCK/0000000003监听到前一个节点0000000002删除事件
2018-08-08 17:23:58.257 - 线程pool-2-thread-10释放分布式锁成功,当前子节点/ROOT_LOCK/0000000002
2018-08-08 17:23:58.257 - 线程pool-2-thread-2获取到分布式锁,当前子节点/ROOT_LOCK/0000000003
2018-08-08 17:23:58.868 - 线程pool-2-thread-2开始释放分布式锁,当前子节点/ROOT_LOCK/0000000003
2018-08-08 17:23:59.119 - 当前节点/ROOT_LOCK/0000000004监听到前一个节点0000000003删除事件
2018-08-08 17:23:59.120 - 线程pool-2-thread-9获取到分布式锁,当前子节点/ROOT_LOCK/0000000004
2018-08-08 17:23:59.120 - 线程pool-2-thread-2释放分布式锁成功,当前子节点/ROOT_LOCK/0000000003
2018-08-08 17:23:59.230 - 线程pool-2-thread-9开始释放分布式锁,当前子节点/ROOT_LOCK/0000000004
2018-08-08 17:23:59.286 - 当前节点/ROOT_LOCK/0000000005监听到前一个节点0000000004删除事件
2018-08-08 17:23:59.286 - 线程pool-2-thread-9释放分布式锁成功,当前子节点/ROOT_LOCK/0000000004
2018-08-08 17:23:59.286 - 线程pool-2-thread-4获取到分布式锁,当前子节点/ROOT_LOCK/0000000005
2018-08-08 17:23:59.641 - 线程pool-2-thread-4开始释放分布式锁,当前子节点/ROOT_LOCK/0000000005
2018-08-08 17:23:59.675 - 当前节点/ROOT_LOCK/0000000006监听到前一个节点0000000005删除事件
2018-08-08 17:23:59.675 - 线程pool-2-thread-4释放分布式锁成功,当前子节点/ROOT_LOCK/0000000005
2018-08-08 17:23:59.676 - 线程pool-2-thread-3获取到分布式锁,当前子节点/ROOT_LOCK/0000000006
2018-08-08 17:24:00.214 - 线程pool-2-thread-3开始释放分布式锁,当前子节点/ROOT_LOCK/0000000006
2018-08-08 17:24:00.299 - 当前节点/ROOT_LOCK/0000000007监听到前一个节点0000000006删除事件
2018-08-08 17:24:00.299 - 线程pool-2-thread-3释放分布式锁成功,当前子节点/ROOT_LOCK/0000000006
2018-08-08 17:24:00.299 - 线程pool-2-thread-7获取到分布式锁,当前子节点/ROOT_LOCK/0000000007
2018-08-08 17:24:00.365 - 线程pool-2-thread-7开始释放分布式锁,当前子节点/ROOT_LOCK/0000000007
2018-08-08 17:24:00.428 - 当前节点/ROOT_LOCK/0000000008监听到前一个节点0000000007删除事件
2018-08-08 17:24:00.429 - 线程pool-2-thread-7释放分布式锁成功,当前子节点/ROOT_LOCK/0000000007
2018-08-08 17:24:00.429 - 线程pool-2-thread-6获取到分布式锁,当前子节点/ROOT_LOCK/0000000008
2018-08-08 17:24:00.538 - 线程pool-2-thread-6开始释放分布式锁,当前子节点/ROOT_LOCK/0000000008
2018-08-08 17:24:00.564 - 当前节点/ROOT_LOCK/0000000009监听到前一个节点0000000008删除事件
2018-08-08 17:24:00.564 - 线程pool-2-thread-6释放分布式锁成功,当前子节点/ROOT_LOCK/0000000008
2018-08-08 17:24:00.564 - 线程pool-2-thread-5获取到分布式锁,当前子节点/ROOT_LOCK/0000000009
2018-08-08 17:24:00.646 - 线程pool-2-thread-5开始释放分布式锁,当前子节点/ROOT_LOCK/0000000009
2018-08-08 17:24:00.674 - 线程pool-2-thread-5释放分布式锁成功,当前子节点/ROOT_LOCK/0000000009
观察测试结果,节点/ROOT_LOCK/0000000000是最小的临时顺序节点,该节点创建成功后就获取到了分布式锁。其他临时顺序节点则监听其前一个节点的删除事件,其前一个节点删除时,就去竞争分布式锁。
测试过程中也发现,10个线程并行竞争分布式锁时,执行checkRootNode()方法会有9个线程捕获到NodeExistsException,这是因为第一个线程创建根节点成功后,其他根节点再去创建根节点就失败了。
2018-08-08 17:23:55.993 - 创建根节点出现异常:
org.apache.zookeeper.KeeperException$NodeExistsException: KeeperErrorCode = NodeExists for /ROOT_LOCK
at org.apache.zookeeper.KeeperException.create(KeeperException.java:119)
at org.apache.zookeeper.KeeperException.create(KeeperException.java:51)
at org.apache.zookeeper.ZooKeeper.create(ZooKeeper.java:783)
at com.example.demo1.distribute.lock.DistributeLock.checkRootNode(DistributeLock.java:57)
at com.example.demo1.distribute.lock.TestDistributeLock$1.run(TestDistributeLock.java:32)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
2018-08-08 17:23:55.993 - 创建根节点出现异常:
至此,基于ZooKeeper实现分布式锁介绍完毕。笔主水平有限,笔误或者不当之处还请批评指正。