1、Zookeeper简介
1.1、概述
ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。
ZooKeeper的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
1.2、数据结构
在Zookeeper中包括了四种节点类型:临时节点、持久节点、临时有序节点(分布式锁基于该类的数据节点实现)、持久有序节点。
1.3、作为分布式锁依据的特性
基于zookeeper的临时有序节点实现分布式锁方案,主要是因为Zookeeper具体如下特性:
- 临时有序节点在客户端与Zookeeper断开连接后,后自动删除,这样可以避免死锁。
- 临时有序节点是生成的节点序号是有序的,我们可以选择最小序号的节点作为获取到锁的判断依据
- zookeeper的节点上可以注册上用户事件,这样我们可以实现阻塞的分布式锁,即Zookeeper的观察器,可以监控节点变化。
- Zookeeper可以做高可用集群,保证可用性。
2、实现分布式锁的步骤
&esmp;大概流程如下:
- 开始时,并发线程(A、B、C……)创建临时有序节点,得到有序的节点集合
- 选取创建节点的序号最小的线程获取锁,其他失败线程,根据序号顺序,监听自己前面一个序号的节点
- 获取到锁的线程,执行业务,完成后释放锁
- 当释放锁的时候,会删除当前有序临时节点,这个时候会触发监听器,唤醒下一个线程继续执行
- 依次执行,知道所有线程执行完成。
3、基于Zookeeper实现分布式锁(排它锁)
3.1、环境搭建
首先,添加Zookeeper需要的依赖,这里需要注意:排除Zookeeper依赖中的日志依赖,否则会和SpringBoot默认的日志有冲突(当然也可以根据项目情况自行选取)。
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.5.8</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
然后,添加zk的连接配置,并编写java类读取。application.properties配置文件中的配置:
server.port=8080
#zookeeper配置
zk.connectString=127.0.0.1:2181
zk.sessionTimeout=10000
#日志配置
logging.level.com.qriver.distributedlock=debug
读取配置的java类:
@Configuration
public class ZookeeperConfig {
@Value("${zk.connectString:127.0.0.1:2181}")
private String connectString;
@Value("${zk.sessionTimeout:10000}")
private int sessionTimeout;
public String getConnectString() {
return connectString;
}
public void setConnectString(String connectString) {
this.connectString = connectString;
}
public int getSessionTimeout() {
return sessionTimeout;
}
public void setSessionTimeout(int sessionTimeout) {
this.sessionTimeout = sessionTimeout;
}
}
再然后,编写基于Zookeeper的分布式锁,这个是核心实现,如下:
/**
* 基于Zookeeper实现分布式锁
*/
public class ZookeeperLock implements AutoCloseable, Watcher {
private ZooKeeper zk;
//创建的临时节点
private String zkNode;
public ZookeeperLock(ZookeeperConfig config) throws IOException {
this.zk = new ZooKeeper(config.getConnectString(),config.getSessionTimeout(),this);
}
public boolean getLock(String key){
try {
//首先,判断业务节点(持久节点)是否存在,不存在就创建
Stat stat = zk.exists("/" + key, false);
if(stat == null){
zk.create("/" + key,key.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
}
//创建临时有序节点
zkNode = zk.create("/" + key + "/" + key + "_",key.getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL_SEQUENTIAL);
//排序,选取最小节点,即查看业务节点下所有节点,排序,选第一个
List<String> childrenNodes = zk.getChildren("/" + key, false);
Collections.sort(childrenNodes);
//获取序号最小的(第一个)子节点
String firstNode = childrenNodes.get(0);
//判断当前节点是否是第一个,即是否应该获取到锁
if (zkNode.endsWith(firstNode)){
return true;
}
//如果当前节点不能获取到锁,则需要添加监听器。
//lastNode,遍历过程中,保存当前节点的前一个节点,并添加监听器
String lastNode = firstNode;
for (String node : childrenNodes){
if (zkNode.endsWith(node)){
zk.exists("/" + key + "/" + lastNode,true);
break;
}else {
lastNode = node;
}
}
//锁定当前对象,
synchronized (this){
wait();
}
return true;
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
@Override
public void close() throws Exception {
zk.delete(zkNode,-1);
zk.close();
}
/**
* 监听器方法,监听到删除节点操作(即释放锁),唤醒下一个节点(线程)
* @param event
*/
@Override
public void process(WatchedEvent event) {
if (event.getType() == Event.EventType.NodeDeleted){
synchronized (this){
notify();
}
}
}
}
最后,编写应用分布式锁的测试类DemoController,如下:
@RestController
public class DemoController {
private Logger logger = LoggerFactory.getLogger(DemoController.class);
@Autowired
private ZookeeperConfig zookeeperConfig;
@RequestMapping("zkLock")
public String testLock() {
logger.debug("进入testLock()方法;");
//“order"表示业务模块
try(ZookeeperLock lock = new ZookeeperLock(zookeeperConfig)){
if(lock.getLock("order")){
logger.debug("获取到锁;");
Thread.sleep(20 * 1000);
}
}catch (Exception e){
e.printStackTrace();
}
logger.debug("方法执行完成;");
return "方法执行完成";
}
}
完成了上述编码工作后,开始进行分布式锁验证,和前面类似,通过启动两个服务,分别访问,后访问的会等到前面访问的释放锁后,才能够获取到锁(即支持阻塞的锁),这里不再粘贴具体的测试结果和打印的日志。
4、基于Curator实现分布式锁(可重入排它锁)
4.1、Curator简介
Curator是Netflix公司开源的一套zookeeper客户端框架,解决了很多Zookeeper客户端非常底层的细节开发工作,包括连接重连、反复注册Watcher、分布式锁、 leader 选举、分布式队列和NodeExistsException异常等。其中,Curator提供了以下几种锁的方案:
- InterProcessMutex:分布式可重入排它锁
- InterProcessSemaphoreMutex:分布式排它锁
- InterProcessReadWriteLock:分布式读写锁
- InterProcessMultiLock:将多个锁作为单个实体管理的容器
接下来,我们主要学习InterProcessMutex可重入排它锁。
4.2、引入Curator依赖
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.5.8</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version>
</dependency>
4.3、配置Curator,引入CuratorFramework对象
&esmp;该对象主要在创建锁的时候需要使用。注入方式如下:
@Configuration
public class CuratorConfig {
@Value("${zk.connectString:127.0.0.1:2181}")
private String connectString;
/**
* 注入CuratorFramework对象,用来创建分布式锁,其中start是初始化方法,close是注销方法
* @return
*/
@Bean(initMethod="start",destroyMethod = "close")
public CuratorFramework getCuratorFramework() {
//设置重试规则
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
//Zookeeper客户端
CuratorFramework client = CuratorFrameworkFactory.newClient(connectString, retryPolicy);
return client;
}
}
4.4、分布式锁的应用
&esmp;在DemoController类中,注入CuratorFramework对象,然后添加一个curatorLock方法用来测试分布式锁。如下:
@Autowired
private CuratorFramework curatorFramework;
@RequestMapping("curatorLock")
public String curatorLock() {
logger.debug("curatorLock()方法;");
//创建锁对象
InterProcessMutex lock = new InterProcessMutex(curatorFramework,"/order");
try{
//获取锁
if(lock.acquire(30, TimeUnit.SECONDS)){
logger.debug("获取到锁;");
Thread.sleep(20 * 1000);
}
}catch (Exception e){
e.printStackTrace();
}finally {
logger.debug("释放锁;");
try {
//释放锁
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
logger.debug("方法执行完成;");
return "方法执行完成";
}
4.5、测试
上述InterProcessMutex分布式锁,也是支持阻塞的锁,所以测试的时候,第二个请求会在第一个请求释放锁后,获取到锁,然后再执行业务逻辑。测试方法和前面类似,这里不再赘述。
5、总结
我们开始直接基于Zookeeper实现的分布式锁,需要我们自己去实现分布式锁的定义,这样会增加工作量,同时我们自己编写的分布式锁可能还会存在较多的bug或问题。而Curator框架为我们封装好了各类的分布式锁,我们可以根据实际需求,选择使用,这样不仅减少开发工作量,同时Curator框架实现的分布式锁,无论从性能上,质量上,肯定也会比我们自己编写的锁更加适用。关于Curator框架的其他锁的使用方法,可以参考《Curator实现分布式锁》。