文章目录
- 一、MAC安装
- 二、Zookeeper
-
- 一). 基本指令. 基本指令)
- 二). Znode节点类型. Znode节点类型)
- 三) 节点的事件监听 一次性 节点的事件监听 一次性)
- 三) zookeeper的java客户端 zookeeper的java客户端)
- 三、Zookeeper分布式锁
-
- [1. 独占排它锁 自旋锁(其他线程在不停重试)](#1. 独占排它锁 自旋锁(其他线程在不停重试))
- [2. 阻塞锁 临时序列化节点(自旋太耗性能)](#2. 阻塞锁 临时序列化节点(自旋太耗性能))
- [3. 可重入锁](#3. 可重入锁)
- [4. 和redis分布式锁对比](#4. 和redis分布式锁对比)
一、MAC安装
- 安装zookeeper
brew install zookeeper
配置文件目录:
/usr/local/etc/zookeeper
其他文件目录:
/usr/local/Cellar/zookeeper
- 修改配置文件
java
// 心跳时间 单位:毫秒
tickTime=2000
// 初始通信时限
initLimit=10
// 同步通信时限
syncLimit=5
// 数据目录,存储内存数据库序列化后的快照路径,也是唯一需要修改的地方
dataDir=/usr/local/Cellar/zookeeper/3.7.0_1/data
// 端口
clientPort=2181
只需要修改dataDir目录就行,然后重启
brew services restart zookeeper
- 进入zookeeper客户端
cd /usr/local/Cellar/zookeeper
//找到对应bin目录
cd bin
// 进入脚本即 zkCli
zkCli
二、Zookeeper
类似Linux的目录结构
一). 基本指令
- ls 查看子节点
ls (path) 查看某一路径的子节点
-
get 查看节点的数据
-
create 创建某节点,不能越级创建,只能一层层目录创建
也可以加参数,这样直接就在该节点下存储了数据
如果要修改该节点的数据值,不能用create再次覆盖,而要用set
-
delete 删除某节点
如果要删除掉的节点有子节点,那就用deleteall
-
set 修改已存在的某节点(只能修改已存在的节点)
已存在的节点成功修改数据。
二). Znode节点类型
-
永久节点 create /path content
-
临时节点 create -e /path content
只要客户端程序断开连接,会自动删除
-
永久序列化节点 create -s /path content
同样的一个节点可创建多个,每个节点后自动生成序列号
-
临时序列化节点 create -s -e /path content (-s -e 顺序前后都可)
会自动生成序列号,只要客户端程序断开连接会自动删除
三) 节点的事件监听 一次性
-
节点创建 nodeCreated
stat -w /xx 监听/xx目录节点的创建
A先开启一个对/watch的监听
B创建/watch目录
再看A
-
节点删除 nodeDeleted
stat -w /xx 监听/xx目录节点的删除
重新监听该节点的删除(由于节点监听是一次性的)
-
节点数据变化 NodeDataChanged
get -w /xx
A监听/bb节点的数据变化
B修改节点数据
再看A
- 子节点变化 NodeChildrenChanged
ls -w /xx
三) zookeeper的java客户端
官方提供、ZkClient、Curator
简单客户端实现
- 引入pom.xml
java
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.7.0</version>
</dependency>
- 代码实现 新建ZkTest
java
package com.distributed.lock.zk;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import java.io.IOException;
public class ZkTest {
public static void main(String[] args) throws InterruptedException {
ZooKeeper zooKeeper = null;
try {
zooKeeper = new ZooKeeper("127.0.0.1:2181", 30000, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
System.out.println("获取链接了");
}
});
System.out.println("一顿操作");
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if(zooKeeper != null){
zooKeeper.close();
}
}
}
}
- 启动测试:
提示jar包重复依赖
把pom.xml新添的zookeeper依赖修改下:
java
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.7.0</version>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
再启动测试:
发现问题,先一顿操作,再获取链接,且获取了两次。
- 先输出了一顿操作
添加CountDowLatch, 等获取链接后再开始操作
CountDownLatch:允许一个或者多个线程去等待其他线程完成操作
CountDownLatch接收一个int型参数,表示要等待的工作线程的个数。
await(): 使当前线程进入同步队列进行等待,直到latch的值被减到0或者当前线程被中断,当前线程就会被唤醒。
countDown(): 使latch的值减1,如果减到了0,则会唤醒所有等待在这个latch上的线程。
那就是先初始化
java
CountDownLatch countDownLatch = new CountDownLatch(1);
获取链接后
java
countDownLatch.countDown();
在一顿操作的前面加上await(),直到获取了链接才准开始操作
java
countDownLatch.await();
- 监听到了两次获取链接
获取链接时加个参数看看
java
System.out.println("获取链接了" + watchedEvent);
很明显,是关闭的时候watcher也监听到了。
watchedEvent有多个状态
判断下状态是不是我要的,再输出"获取链接了"即可。即:只监听想监听到的状态。
java
Event.KeeperState state = watchedEvent.getState();
if(Event.KeeperState.SyncConnected.equals(state)){
System.out.println("获取到链接了" + watchedEvent);
}else if(Event.KeeperState.Closed.equals(state)) {
System.out.println("关闭链接了" + watchedEvent);
}
重启测试:
成功。
CRUD操作
- 节点新增
java
String create(String path, byte[] data, List<ACL> acl, CreateMode createMode)
path 路径
data 数据 要求是字节,字符串后加getBytes()
acl 权限 OPEN_ACL_UNSAFE 所有人都可以操作即可
createMode 创建类型
CreateMode.PERSISTENT 永久节点
CreateMode.EPHEMERAL 临时节点
CreateMode.PERSISTENT_SEQUENTIAL 永久序列化节点
CreateMode.EPHEMERAL_SEQUENTIAL 临时序列化节点
java
zooKeeper.create("/ustc","hello zookeeper".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
启动成功,查看zookeeper
- 节点查询
java
// 判断节点是否存在
Stat stat = zooKeeper.exists("/ustc",false);
if(stat != null){
System.out.println("当前节点存在");
}else{
System.out.println("当前节点不存在");
}
// 获取当前节点中的数据内容
byte[] data = zooKeeper.getData("/ustc",false,stat);
System.out.println("当前节点内容" + new String(data));
// 获取当前子节点
List<String> children = zooKeeper.getChildren("/ustc",false);
System.out.println("当前子节点:" + children);
- 节点更新
java
// 更新:版本号必须和当前节点的版本号一致,否则更新失败。也可以指定为-1,代表不关心版本号
zooKeeper.setData("/ustc","bye bye".getBytes(), stat.getVersion());
- 删除节点
java
// 删除
zooKeeper.delete("/ustc", -1);
监听
监听子节点
java
// 获取当前子节点
List<String> children = zooKeeper.getChildren("/ustc",true);
同时保证main方法不能结束,最后一行添加
java
System.in.read();
启动成功,去修改节点值看能否监听到.
这种监听方式只能生效一次。
也可以自己加个new watcher(),不过也是只生效一次。
想一直生效可以把它提取出来当作方法,然后最后递归的调用自己。
java
// 监听当前子节点
List<String> children = zooKeeper.getChildren("/ustc", new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
System.out.println("当前子节点发生变化....");
}
});
三、Zookeeper分布式锁
1. 独占排它锁 自旋锁(其他线程在不停重试)
zookeeper的create方法无法覆盖新的路径,只能成功一次 ,可以依靠这个特效来实现独占排它锁
多个线程去create一个path的时候,只有一个线程可以create成功,也就相当于抢到了锁,其他线程只能不停的尝试create,而抢到锁的线程A,在执行完后释放锁,即delete path,就会有下一个线程create成功。
为了避免出现,抢到锁后客户端宕机,导致锁无法释放,即拿锁线程不会delete path,create的节点设置为临时节点,这样当客户端宕机时,节点也跟着自动销毁,也就解锁了。
代码实现
- ZkDistributedLock
java
public class ZkDistributedLock implements Lock {
private ZooKeeper zooKeeper;
private String lockName;
// 根节点
private static final String ROOT_PATH = "/locks";
public ZkDistributedLock(ZooKeeper zooKeeper, String lockName){
this.zooKeeper = zooKeeper;
this.lockName = lockName;
try {
// 保证根节点存在
if(zooKeeper.exists(ROOT_PATH,false) == null){
zooKeeper.create(ROOT_PATH,null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
} catch (KeeperException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
public void lock() {
this.tryLock();
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
// 创建znode节点过程
try {
// 临时节点,避免zk客户端获取到锁后,宕机带来的死锁问题,相当于redis的过期时间
this.zooKeeper.create(ROOT_PATH + "/" + lockName, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
return true;
} catch (Exception e) {
// 出问题后一直递归调用,不停尝试
try {
Thread.sleep(100);
this.tryLock();
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
// 删除znode节点
try {
this.zooKeeper.delete(ROOT_PATH+"/"+lockName,-1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (KeeperException e) {
throw new RuntimeException(e);
}
}
@Override
public Condition newCondition() {
return null;
}
}
- ZkClient
java
@Component
public class ZkClient {
public ZooKeeper zooKeeper;
/**
* 在无参构造后立刻执行
* 获取联建,项目启动时
*/
@PostConstruct
public void init(){
CountDownLatch countDownLatch = new CountDownLatch(1);
try {
zooKeeper = new ZooKeeper("127.0.0.1:2181", 30000, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
Event.KeeperState state = watchedEvent.getState();
if (Event.KeeperState.SyncConnected.equals(state) && Event.EventType.None.equals(watchedEvent.getType())) {
System.out.println("获取到链接了" + watchedEvent);
} else if (Event.KeeperState.Closed.equals(state)) {
System.out.println("关闭链接了" + watchedEvent);
} else {
System.out.println("节点事件....");
}
countDownLatch.countDown();
}
});
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* 释放zk的链接
*/
@PreDestroy
public void destroy(){
if(zooKeeper != null){
try {
zooKeeper.close();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public ZkDistributedLock getLock(String lockName){
return new ZkDistributedLock(zooKeeper,lockName);
}
}
- StockService
java
public void deduct() {
ZkDistributedLock lock = zkClient.getLock("lock");
lock.lock();
try{
// 1。 查询库存
String stockStr = redisTemplate.opsForValue().get("stock");
if (!StringUtil.isNullOrEmpty(stockStr)) {
int stock = Integer.parseInt(stockStr);
// 2。 判断条件是否满足
if (stock > 0) {
// 3 更新redis
redisTemplate.opsForValue().set("stock", String.valueOf(stock - 1));
}
}
}finally {
lock.unlock();
}
}
简单来说,就是create实现排它锁,加锁是create临时节点,解锁就是delete临时节点.
jmeter测试:
并发量高达700,库存也成功清0
2. 阻塞锁 临时序列化节点(自旋太耗性能)
思路:通过创建临时序列化节点,每个线程创建的节点序列号依次递增。
最小的节点序列号抢到锁,比如0,然后1监听比自己小的序列化节点也就是0,2监听比自己小的节点1,如此下去,相当于阻塞着排队等待。同时也是公平锁。
1)所有请求要求获取锁时,给每一个请求创建临时序列化节点
2)获取当前节点的前置节点(比自己序列号小1),如果前置节点为空,则获取锁成功。否则监听前置节点。
把根节点的孩子节点全部拿到,然后通过排序比较,拿到前置节点。
3)获取锁成功后执行业务操作,然后释放当前节点的锁。
代码实现
java
public class ZkDistributedLock implements Lock {
private ZooKeeper zooKeeper;
private String lockName;
// 前置节点路径
private String currentNodePath;
// 根节点
private static final String ROOT_PATH = "/locks";
public ZkDistributedLock(ZooKeeper zooKeeper, String lockName){
this.zooKeeper = zooKeeper;
this.lockName = lockName;
try {
// 保证根节点存在
if(zooKeeper.exists(ROOT_PATH,false) == null){
zooKeeper.create(ROOT_PATH,null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
} catch (KeeperException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
public void lock() {
this.tryLock();
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
/**
*
* @return true 成功获取锁
*/
@Override
public boolean tryLock() {
// 创建znode节点过程
try {
// 所有请求要求获取锁时,给每一个请求创建临时序列化节点,currentNodePath返回的是全路径
currentNodePath = this.zooKeeper.create(ROOT_PATH + "/" + lockName + "-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// 获取前置节点,若前置节点为空,则获取锁成功,否则监听前置节点
String preNode = this.getPreNode();
// 因为获取前置节点的操作不具有原子性,故再次判断zk中的前置节点是否存在
if(preNode != null){
// 利用闭锁思想,实现阻塞功能
CountDownLatch countDownLatch = new CountDownLatch(1);
if(this.zooKeeper.exists(ROOT_PATH + "-" + preNode, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
// 监听是否存在,若不存在则触发
countDownLatch.countDown();
}
}) == null){
// 若前置节点不存在了,我就是第一个,直接获取锁
return true;
}
// 不能直接返回true,需要等待监听到存在的状态变化
countDownLatch.await();
}
return true;
} catch (Exception e) {
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
// 删除znode节点
try {
this.zooKeeper.delete(ROOT_PATH+"/"+lockName,-1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (KeeperException e) {
throw new RuntimeException(e);
}
}
@Override
public Condition newCondition() {
return null;
}
private String getPreNode(){
// 获取根节点下的所有节点
try {
List<String> children = this.zooKeeper.getChildren(ROOT_PATH,false);
Assert.notEmpty(children,"根节点下没有子节点!");
// 删选出lockName对应的子节点
List<String> nodes = children.stream().filter(
node ->
StringUtils.startsWithIgnoreCase(node,lockName+"-")
).collect(Collectors.toList());
Assert.notEmpty(children,"没有该锁的子节点!");
// 排好队
Collections.sort(nodes);
// 获取当前节点的下标
// 获取当前节点的名称 currentNodePath是全路径 但是children不是
String currentNode = StringUtil.substringAfter(currentNodePath,'/');
int index = Collections.binarySearch(nodes,currentNode);
if(index < 0){
throw new IllegalMonitorStateException("没有当前锁的子节点");
}else if(index > 0){
return nodes.get(index-1); // 若不是最小的节点,返回前置节点
}
return null; // 如果当前节点没有前置节点,即是第一个节点,返回null
} catch (Exception e) {
throw new RuntimeException("非法操作");
}
}
}
Jmeter测试
并发达到了600,略低于独占排它锁。库存也成功清0。
3. 可重入锁
思路:通过ThreadLocal保存线程的入锁次数
如果ThreadLocal中数值>0,说明已经入锁,可重入,直接+1,抢锁成功
如果ThreadLocal无数值,那就按照之前的步骤去抢锁。
在抢到锁后ThreadLocal数值设置为1。
解锁的时候将ThreadLocal数值-1,如果是0就delete删除节点,即释放锁。
java
package com.distributed.lock.zk;
import cn.hutool.core.lang.Assert;
import io.netty.util.internal.StringUtil;
import org.apache.zookeeper.*;
import org.springframework.util.StringUtils;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.stream.Collectors;
public class ZkDistributedLock implements Lock {
private ZooKeeper zooKeeper;
private String lockName;
// 前置节点路径
private String currentNodePath;
// 根节点
private static final String ROOT_PATH = "/locks";
public static final ThreadLocal<Integer> THREAD_LOCAL = new ThreadLocal<>();
public ZkDistributedLock(ZooKeeper zooKeeper, String lockName){
this.zooKeeper = zooKeeper;
this.lockName = lockName;
try {
// 保证根节点存在
if(zooKeeper.exists(ROOT_PATH,false) == null){
zooKeeper.create(ROOT_PATH,null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
} catch (KeeperException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
public void lock() {
this.tryLock();
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
/**
*
* @return true 成功获取锁
*/
@Override
public boolean tryLock() {
// 创建znode节点过程
try {
//判断ThreadLocal中是否已经有锁,如果有锁直接重入(+1),返回true,重入成功
Integer num = THREAD_LOCAL.get();
if( num != null || num > 0){
THREAD_LOCAL.set(num+1);
return true;
}
// 所有请求要求获取锁时,给每一个请求创建临时序列化节点,currentNodePath返回的是全路径
currentNodePath = this.zooKeeper.create(ROOT_PATH + "/" + lockName + "-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// 获取前置节点,若前置节点为空,则获取锁成功,否则监听前置节点
String preNode = this.getPreNode();
// 因为获取前置节点的操作不具有原子性,故再次判断zk中的前置节点是否存在
if(preNode != null){
// 利用闭锁思想,实现阻塞功能
CountDownLatch countDownLatch = new CountDownLatch(1);
if(this.zooKeeper.exists(ROOT_PATH + "-" + preNode, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
// 监听是否存在,若不存在则触发
countDownLatch.countDown();
}
}) == null){
// 若前置节点不存在了,我就是第一个,直接获取锁
THREAD_LOCAL.set(1);
return true;
}
// 不能直接返回true,需要等待监听到存在的状态变化
countDownLatch.await();
}
THREAD_LOCAL.set(1);
return true;
} catch (Exception e) {
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
// 删除znode节点
try {
// 可重入锁,先删除Thread_Local中的数值
THREAD_LOCAL.set(THREAD_LOCAL.get()-1);
if(THREAD_LOCAL.get() == 0){
this.zooKeeper.delete(ROOT_PATH+"/"+lockName,-1);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (KeeperException e) {
throw new RuntimeException(e);
}
}
@Override
public Condition newCondition() {
return null;
}
private String getPreNode(){
// 获取根节点下的所有节点
try {
List<String> children = this.zooKeeper.getChildren(ROOT_PATH,false);
Assert.notEmpty(children,"根节点下没有子节点!");
// 删选出lockName对应的子节点
List<String> nodes = children.stream().filter(
node ->
StringUtils.startsWithIgnoreCase(node,lockName+"-")
).collect(Collectors.toList());
Assert.notEmpty(children,"没有该锁的子节点!");
// 排好队
Collections.sort(nodes);
// 获取当前节点的下标
// 获取当前节点的名称 currentNodePath是全路径 但是children不是
String currentNode = StringUtil.substringAfter(currentNodePath,'/');
int index = Collections.binarySearch(nodes,currentNode);
if(index < 0){
throw new IllegalMonitorStateException("没有当前锁的子节点");
}else if(index > 0){
return nodes.get(index-1); // 若不是最小的节点,返回前置节点
}
return null; // 如果当前节点没有前置节点,即是第一个节点,返回null
} catch (Exception e) {
throw new RuntimeException("非法操作");
}
}
}
测试后
吞吐量高达700,库存也成功清0。
4. 和redis分布式锁对比
-
Zookeeper分布式锁:
独占排它
阻塞锁
可重入
-
redis分布式锁
-
独占排它互斥
redis: setnx
zk: 节点不重复
-
防死锁:客户端获取到锁后服务器宕机
redis: 过期时间
zk-临时节点:一旦客户端服务器宕机,链接就会关闭,此时zk心跳检测不到客户端程序,删除对应的临时节点
-
可重入
redis: hash脚本+lua脚本
zk: ThreadLocal保存入锁次数/节点数据/concurrentHashMap
-
防误删
redis: 唯一ID
zk: 每个请求线程创建一个唯一的序列化节点
-
原子性
redis:
zk: 创建、删除、查询及监听具备原子性
-
自动续期
redis: Timer定时器
zk: 临时节点,没有过期时间,不需要自动续期
-
单机故障问题
redis: 会,主从需要时间
zk: 一般都是集群部署,偏向于一致性的集群
-
阻塞锁
zk: 临时节点序列化,监听上一个小的节点,实现排队阻塞,实现公平锁
但redis需要不停自旋尝试,是非公平锁