Zookeeper ----- 3.6.2版本
概述
分布式读写锁地址.
zookeeper 是基于 观察者模式
设计的分布式服务管理框架,负责存储大家都关心的数据,接受观察者的注册
,一旦这些数据发生变化,就通知注册的观察者
特点
- ZK集群: 一个leader 和多个follower 组成
- 集群中只要有半数以上的节点存活,zk就能对外提供服务
- 全局数据一致,无论哪个节点,数据都是一致的。
- 更新请求顺序执行,来自同一个client的请求按照发送顺序执行
- 更新的原子性,更新请求要么成功,要么失败
- 一定时间范围内,client能读到最新数据
数据结构
Zookeeper 数据模型整体可以看成是一个树,每个节点称为一个ZNode . 每个ZNode 默认能够存储 1MB
数据,每个ZNode 都可以 通过其路径唯一标识
应用场景
统一命名服务,统一配置管理,统一集群管理,服务器节点动态上下线,软负载均衡
安装
单机版
- zookeeper下载地址. 下载并上传到linux
- 修改配置文件
zoo_sample.cfg
为zoo.cfg
- 修改 zoo.cfg 下的 dataDir=/data/zookeeper/
- 相应位置创建文件夹
- ./zkServer.sh start 启动 zookeeper
- ./zkCli.sh 启动zookeeper 客户端
分布式版本
- 拷贝zk到其他服务器上
- 在 dataDir 的地方创建一个 vim myid 填写
服务器id 2,3,4 之类的
- 修改conf/zoo.cfg 配置文件 添加 配置参数
server.A=B:C:D
server.2=HBASE01:2888:3888
server.3=HBASE02:2888:3888
server.4=HBASE03:2888:3888
A: 是一个数字,也就是 集群服务器 myid 填写的数字
B: 服务器的IP地址
C: 该集群 leader
服务器 交换信息
的端口
D: 如果leader挂了,需要另一个端口来进行选举,是选举的端口
- 启动zk
配置参数
tickTime = 2000 通信心跳数,客户端和服务端心跳时间,毫秒
initLimit=10 初始通信时限 initLimix * tickTime 超过这个时间,leader 认为 follower 已经死了
syncLimit = 5 启动之后的leader 和follower 的时限
节点类型
持久性
- 客户端和服务器断开连接后,创建的节点不删除
- 持久化还有顺序目录节点,ZNode 后面还会附带单调递增的计数器,由父节点维护.
短暂 - 断开连接后就被删除
- 临时节点带顺序的
客户端和服务器断开连接后,创建的节点自己删除
客户端shell命令
ls path
ZNode 信息 例如 ls / 查看 根节点下的ZNode 默认 有一个zookeepercreate /path/ZNode value
创建一个节点 不能跨级创建 create /school tom 根节点下创建了一个school节点,节点的值是tomget /path/node
获得节点内的值create -e /node
创建短暂的节点,当客户端断开连接后,该节点消失
create -s /node
获得后面带有计数器的节点,本来同名的节点是不能创建,但是-s之后创建的名称后面会有序号set /path/node value
给某个节点设置值get -w /path/node
监听某个节点的变化,当发生变化的时候会收到通知,只有一次ls -w /path
监听某个路径下变化,该路径下节点发生变化接受通知,子节点变化不会有反应delete /node
删除节点
10.deleteall /path
递归删除
watch,事件监听机制
特性 | 说明 |
---|---|
一次性 | watcher 是一次性的,收到了通知之后需要重新注册 |
客户端顺序回调 | 不知道什么意思 |
轻量级 | 回调参数里面只包含通知状态,时间类型和节点的路径。 |
时效性 | watch在session失效的时候彻底失效 |
watcher 接口设计
任何实现了watcher 接口的都是一个新的watcher . watcher 内部包含了两个枚举类型 KeeperState
和EventType
. 在watcher 回调函数的参数里面包含了这两个类型
KeeperState
枚举属性 | 说明 |
---|---|
SyncConnected | 客户端正常连接 |
DisConnected | 断开连接 |
Exipred | 会话session失效 |
AuthFailed | 身份认证失败 |
EventType
枚举属性 | 说明 |
---|---|
None | 无 |
NodeCreated | 监听的节点数据被创建 |
NodeDelete | 监听的节点被删除 |
NodeDataChanged | 节点数据发生变更 |
NodeChildernChanged | 节点数据子节点发生变更 |
session
JAVA 使用zookeeper实现公平和非公平分布式锁
实现思路
创建临时有序节点, 如果是读锁就是 /lock/read_000001
. 如果是写锁就是 /lock/write_0000001
. 创建之后,获得 /lock 下的所有子节点,分成两个数组,包含read 和包含 write 的 .两个数组排序
如果当前是读锁,则获取锁的条件是,write数组没有节点,并且自己前面没有节点.如果有写锁,则监听写锁的最后一个节点的消失.消失之后在重新尝试获取锁.
如果当前是写锁,则只需要判断自己是不是写锁数组里面的第一个,不是的话,就监听前一个写锁的消失
代码
- 引入pom文件
<!-- https://mvnrepository.com/artifact/org.apache.zookeeper/zookeeper -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.6.1</version>
</dependency>
- 代码实现
== 在static静态代码块下,只有等主线程执行结束才会执行子线程,所以如果在static中使用线程通信会导致死锁!==
zk如果和服务器断开链接之后,会在监听器收到回掉,这时候需要重新注册zklclient,阻塞当前方法
package com.zc.lock;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.Closeable;
import java.io.IOException;
import java.util.List;
public abstract class ZkLock implements Closeable {
private static final String ROOTLOCK = "/lock";
protected static final String READ_WRITE_LOCK_PATH = "/lock/readWriteLock";
protected static final String SERVER = "EUREKA01:2181";
protected static final Integer TIMEOUT= 20000;
protected static ZooKeeper zooKeeper;
// 如果断开链接了,就需要全部暂停等待zk锁从新链接成功
private static final Object reconnectLock = new Object();
protected String path ;
Watcher watcher = event -> {
if (event.getType() == Watcher.Event.EventType.None) {
if (event.getState() == Watcher.Event.KeeperState.SyncConnected) {
System.out.println("重新连接成功!");
synchronized (reconnectLock){
reconnectLock.notifyAll();
}
}
}
};
static {
try {
zooKeeper = new ZooKeeper(SERVER, TIMEOUT,event -> {
if (event.getState() == Watcher.Event.KeeperState.SyncConnected) {
System.out.println("重新连接成功!");
synchronized (reconnectLock){
reconnectLock.notifyAll();
}
}
});
}catch (Exception e){
System.out.println("创建zk失败");
}
}
public ZkLock() throws Exception {
init();
}
public void init() throws Exception{
try {
// 创建持久节点 /lock
if (zooKeeper.exists(ROOTLOCK, false) == null){
zooKeeper.create(ROOTLOCK, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
// 创建持久节点 /lock/unfairLock
if (zooKeeper.exists(READ_WRITE_LOCK_PATH, false) == null){
zooKeeper.create(READ_WRITE_LOCK_PATH, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
}catch (Exception e){
System.out.println(e);
if (e instanceof KeeperException.ConnectionLossException){
// zookeeper 服务端断开连接,等待重新链接
synchronized (reconnectLock){
reconnectLock.wait();
}
init();
}
}
}
@Override
public void close() throws IOException {
try {
zooKeeper.delete(path, -1);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
protected void cirLock(String path, List<String> list, int index) throws Exception {
// 监听上一个读锁
String lastPath = list.get(index - 1);
Stat stat = zooKeeper.exists(READ_WRITE_LOCK_PATH + "/" + lastPath, event -> {
// KeeperState DisConnected Exipred 发生,临时节点可能也会被删除
if (event.getType() == Watcher.Event.EventType.NodeDeleted) {
synchronized (this) {
this.notify();
}
}
if (event.getState() == Watcher.Event.KeeperState.Disconnected ||
event.getState() == Watcher.Event.KeeperState.Expired){
System.out.println("掉线了,重新链接");
try {
zooKeeper = new ZooKeeper(SERVER, TIMEOUT,watcher);
} catch (IOException e) {
e.printStackTrace();
}
}
});
if (stat == null) {
// 上一个节点消失了,再次重新获取锁
attemptLock(path);
} else {
// 阻塞,等待锁释放
synchronized (this) {
this.wait();
}
attemptLock(path);
}
}
protected abstract void attemptLock(String path) throws Exception;
}
package com.zc.lock;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.ZooDefs;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/**
* 写锁优先
*/
public class ReadWriteLock extends ZkLock {
private static final String READ_WRITE_NODE = "lock_";
// 有写锁的时候读锁等待,写锁永远都可以插队
public static String READ = "read_";
public static String WRITE = "write_";
// 当前锁的状态
private String state;
public ReadWriteLock(String state) throws Exception {
this.state = state;
// 1. 创建临时有序节点
path = zooKeeper.create(READ_WRITE_LOCK_PATH + "/" + READ_WRITE_NODE + state, "".getBytes()
, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// 2. 获得路径下所有节点
attemptLock(path);
}
@Override
protected void attemptLock(String path) throws Exception {
List<String> list = zooKeeper.getChildren(READ_WRITE_LOCK_PATH, false);
List<String> readList = list.stream().filter(data -> data.contains(READ)).collect(Collectors.toList());
List<String> writeList = list.stream().filter(data -> data.contains(WRITE)).collect(Collectors.toList());
Collections.sort(readList);
Collections.sort(writeList);
if (READ.equals(state)) {
// 读锁的获取方式必须是前面没有写锁,并且自己是第一个,否则阻塞
if (writeList.size() == 0) {
int index = readList.indexOf(path.substring(READ_WRITE_LOCK_PATH.length() + 1));
if (index == 0) {
// System.out.println("获取到锁对象");
return;
} else {
cirLock(path, readList, index);
}
} else {
// 有写锁,需要监听最后一个写锁
cirLock(path, writeList, writeList.size());
}
} else {
// 写锁的获取方式是前面没有写锁
int index = writeList.indexOf(path.substring(READ_WRITE_LOCK_PATH.length() + 1));
if (index == 0) {
// System.out.println("写锁获取到锁对象");
return;
} else {
// 监听上一个读锁
cirLock(path, writeList, index);
}
}
}
}
测试结果
被写锁包裹的方法,每次都可以及时执行,而被读锁包裹的重制方法需要排队等待
/**
* ---------------非公平锁,重制命令每次执行都会优先于读取命令
*/
@GetMapping("/test03")
public void test03(){
for (int i = 0; i < 100; i++) {
new Thread(()->{
for (int i1 = 0; i1 < 10; i1++) {
try (ZkLock lock = new ReadWriteLock(ReadWriteLock.READ)){
Thread.sleep(50);
restTemplate.getForObject("http://PUBLIC/add", String.class);
}catch (Exception e){
}
}
}).start();
}
}
@GetMapping("/test04")
public void test04(){
try (ZkLock lock = new ReadWriteLock(ReadWriteLock.WRITE)){
restTemplate.getForObject("http://PUBLIC/clean", String.class);
}catch (Exception e){
}
}
@GetMapping("/test05")
public void test05(){
try (ZkLock lock = new ReadWriteLock(ReadWriteLock.READ)){
restTemplate.getForObject("http://PUBLIC/clean", String.class);
}catch (Exception e){
}
}
使用curator 优化zookeeper
- 引入pom依赖
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.6.1</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version>
</dependency>
- JAVA 代码编写
CuratorFramework client = CuratorFrameworkFactory.builder()
// IP 地址 + 端口号,多个用逗号隔开
.connectString("localhost:2181")
// 会话超时时间
.sessionTimeoutMs(5000)
// 重连机制
.retryPolicy(new RetryOneTime(3000))
// 命名空间,用该客户端操作的东西都在该节点之下
.namespace("lock")
.build();
client.start();
if (client.getState() == CuratorFrameworkState.STARTED){
System.out.println("启动成功");
}
client.close();
- 重连策略 (有待测试)
new RetryOneTime(3000)
断开链接后3秒重连一次
new RetryNTimes(50,3000)
每3秒连接一次,一共连接50次
new RetryUntilElapsed(10000,3000)
每3秒重联一次,总共10秒后断开连接
测试一次连接策略的时候,连接时长超过了3秒连接还是可以连接的上 - 创建节点
client.create()
.withMode(CreateMode.PERSISTENT)
.withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
.forPath("/node01");
- 递归创建节点(当父节点不存在的时候创建父节点)
client.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.PERSISTENT)
.withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
.forPath("/parent/sonnode");
- 删除节点
client.delete()
.deletingChildrenIfNeeded()
.forPath("/node");
- 判断节点是否存在
client.checkExists().forPath("/dds");
- 监听节点的创建
节点的创建和改变都会触发回调
NodeCache nodeCache = new NodeCache(client, "/msm");
nodeCache.start();
nodeCache.getListenable().addListener(()->{
System.out.println(nodeCache.getCurrentData().getPath());
System.out.println(Arrays.toString(nodeCache.getCurrentData().getData()));
});
使用curator 实现分布式读写锁
由于注册监听是异步的,有可能还没监听上就已经被销毁了,所以在阻塞的时候是要用有超时时间的阻塞
package com.zc.lockv2;
import com.zc.lock.ReadWriteLock;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.api.GetChildrenBuilder;
import org.apache.curator.framework.imps.CuratorFrameworkState;
import org.apache.curator.framework.recipes.cache.ChildData;
import org.apache.curator.framework.recipes.cache.NodeCache;
import org.apache.curator.retry.RetryOneTime;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.ZooDefs;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
/**
* zk 实现读写锁
* 实现效果为: 在lock 节点下创建 自定义锁资源, lock_01 , lock_02 等
* 锁资源下为有序临时节点 分为读节点和写节点 read_00001 write_00001
* 获取读锁的方式为,锁资源下没有写节点,如果有则监听最后一个,读锁之间不会相互竞争
* 获取写锁的方式也是写锁下没有最后一个节点,并且当前有读锁的时候需要监听当前读锁的结束,写锁之间会相互竞争
*/
public class ZkLock {
private static final String ROOTLOCK = "lock";
protected static final String SERVER = "EUREKA01:2181";
protected static final Integer TIMEOUT = 2000000;
protected static final CuratorFramework client;
private String name;
private ReadWriteType readWriteType;
public String path;
// 是否可以获取写锁的标志位,获取写锁的条件是
// 1.处于写锁的第一个,并且当前没有读锁正在读取
// 写锁的第一个可以通过有序数组排序,没有读锁则得通过监听最老的读锁释放之后,修改这个值
// 这个标志为同样可以监听 第二个写锁监听结束后变成第一个写锁的情况.
// 判断是否可以获得写锁的标志就是要么 是 写锁的第一个要么就是上一个监听的回掉生效了
private Boolean shouldWrite = false;
static {
client = CuratorFrameworkFactory.builder()
// IP 地址 + 端口号,多个用逗号隔开
.connectString(SERVER)
// 会话超时时间
.sessionTimeoutMs(TIMEOUT)
// 重连机制
.retryPolicy(new RetryOneTime(10000))
// 命名空间,用该客户端操作的东西都在该节点之下
.namespace(ROOTLOCK)
.build();
client.start();
if (client.getState() == CuratorFrameworkState.STARTED) {
System.out.println("启动成功");
}
}
public ZkLock(String name, ReadWriteType readWriteType) {
this.name = name;
this.readWriteType = readWriteType;
try {
if (client.checkExists().forPath("/" + name) == null) {
client.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.PERSISTENT)
.withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
.forPath("/" + name);
}
} catch (Exception e) {
}
}
public void lock() throws Exception {
path = client.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
.forPath("/" + name + "/" + readWriteType.type);
attemptLock(path);
}
public void unLock() {
try {
client.delete()
.deletingChildrenIfNeeded()
.forPath(path);
} catch (Exception e) {
e.printStackTrace();
}
}
protected void attemptLock(String path) throws Exception {
GetChildrenBuilder children = client.getChildren();
List<String> list = children.forPath(getPath());
List<String> writeList = list.stream().filter(data -> data.contains(ReadWriteType.WRITE.type))
.sorted(String::compareTo).collect(Collectors.toList());
List<String> readList = list.stream().filter(data -> data.contains(ReadWriteType.READ.type))
.sorted(String::compareTo).collect(Collectors.toList());
String currentName = path.substring(getPath().length() + 1);
if (readWriteType == ReadWriteType.READ) {
// 读锁判断最后一个写锁没有了就可以获得锁了
if (writeList.size() == 0) {
// 我是读锁,并且没有写锁,直接获得
return;
} else {
// 读锁但是有写锁,监听最后一个写锁
String lastPath = writeList.get(writeList.size() - 1);
cirLock(lastPath);
}
} else {
// 写锁,判断自己是不是第一个,如果不是则必须得等到没有
if (writeList.size() == 1) {
// 获取到锁,已经没人获取到读锁了
if (readList.size() == 0 || shouldWrite) {
return;
} else {
String first = readList.get(0);
cirLock(first);
}
} else {
String name = path.substring(getPath().length() + 1);
if (writeList.lastIndexOf(name) == 0) {
// 获取到锁
if (readList.size() == 0) {
return;
} else {
String first = readList.get(0);
cirLock(first);
}
} else {
// 只需要监听前一个写锁的释放即可
String lastPath = writeList.get(writeList.lastIndexOf(name) - 1);
cirLock(lastPath);
}
}
}
// 没有写锁,全部都不阻塞
}
protected void cirLock(String lastPath) throws Exception {
// 获得上一个锁对象
NodeCache nodeCache = new NodeCache(client, getPath() + "/" + lastPath);
nodeCache.start();
nodeCache.getListenable().addListener(() -> {
ChildData currentData = nodeCache.getCurrentData();
if (currentData == null) {
synchronized (this) {
shouldWrite = true;
notifyAll();
}
}
});
synchronized (this) {
wait(1000);
}
attemptLock(path);
}
public enum ReadWriteType {
READ("read_"),
WRITE("write_");
private String type;
ReadWriteType(String type) {
this.type = type;
}
}
private String getPath() {
return "/" + name;
}
}