1、zookeeper简介
- zookeeper是啥?
zookeeper是为分布式应用而生的,是协调多应用分布式部署共同完成任务的应用程序。管理协同数据的程序。例如访问某个应用接口的数据的过程,你只需要关心应用数据,对于这个数据时从那台服务器上提供的协同数据时有zookeeper来协调处理的。让开发人员更注重应用本身的逻辑关系,而不是应用之间的协同工作。
zookeeper可以在分布式系统中协助多个任务,任务可以理解成多个线程或者多个应用;这些任务可是协助或者为了管理竞争。- zookeeper能干啥?
- 用于存储集群中的共享数据(非海量数据)
- 集群中选取主节点(主从节点选举)
- 检测监控系统的崩溃情况(监听节点,当系统崩溃后,及时通知监控系统)
- 服务发现,数据分片,故障恢复等功能(服务节点监听,新增服务信息,通知服务监控系统 dubbo框架)
- zookeeper有哪些特性?
可靠性:一旦Zookeeper成功的应用了一个事务,并完成对client的响应,那么Zookeeper内部集群的所有服务器的状态都会是一致的保留下来,Zookeeper集群节点之间实现数据的同步,主节点会将接受的数据,同步到从节点上,当大部分节点都已经完成同步后,主节点将数据返回调用者,保证了主从节点之间的数据一致性。
顺序一致性:从一个client发起的请求,最终会严格的按照发起的顺序被应用到Zookeeper中去。实质上,ZK会对每一个client的每一个请求,进行编号,说白了,就是分配一个全局唯一的递增编号,这个编号反映了所有事务操作的先后顺序。
实时性:通常意义下的实时性是指一旦事务被成功应用,那么client会立刻从服务器端获取到变更后的新数据。ZK仅仅能够保证在一定时间内,client最终一定会能从服务器上获取到最新的数据。
高可用:在ZK集群内部,会有一个Leader,多个Follower。一旦Leader挂掉,那么ZK会通过Paxos算法选举出新的Leader,只要ZK集群内部的服务器有一半以上正常工作,那么ZK就能对外正常提供服务!
简单的数据结构:类似于Linux文件系统的树形结构,简单,实用!(树形层次空间)
2、分布式锁
在单体的应用开发场景中,涉及并发同步的时候,大家往往采用synchronized或者Lock的方式来解决多线程间的同步问题。但在分布式集群工作的开发场景中,那么就需要一种更加高级的锁机制,来处理种跨JVM进程之间的数据同步问题,这就是分布式锁。
单体应用应用同步 | 分布式应用同步 |
3、zookeeper功能介绍
- 创建对话
public ZkClient(String serverstring)
public ZkClient(String zkServers, int connectionTimeout)
public ZkClient(String zkServers, int sessionTimeout, int connectionTimeout)
public ZkClient(String zkServers, int sessionTimeout, int connectionTimeout, ZkSerializer zkSerializer)
public ZkClient(final String zkServers, final int sessionTimeout, final int connectionTimeout, final ZkSerializer zkSerializer, final long operationRetryTimeout)
public ZkClient(IZkConnection connection)
public ZkClient(IZkConnection connection, int connectionTimeout)
public ZkClient(IZkConnection zkConnection, int connectionTimeout, ZkSerializer zkSerializer)
public ZkClient(final IZkConnection zkConnection, final int connectionTimeout, final ZkSerializer zkSerializer, final long operationRetryTimeout)
- 创建节点
- 永久节点
public void createPersistent(String path)
public void createPersistent(String path, boolean createParents)
public void createPersistent(String path, boolean createParents, List acl)
public void createPersistent(String path, Object data)
public void createPersistent(String path, Object data, List acl)
- 永久有序节点
public String createPersistentSequential(String path, Object data)
public String createPersistentSequential(String path, Object data, List acl)
- 临时节点
public void createEphemeral(final String path)
public void createEphemeral(final String path, final List acl)
public void createEphemeral(final String path, final Object data)
public void createEphemeral(final String path, final Object data, final List acl)
- 临时有序节点
public String createEphemeralSequential(final String path, final Object data)
public String createEphemeralSequential(final String path, final Object data, final List acl)
- 删除节点
public boolean delete(final String path)
public boolean delete(final String path, final int version)
public boolean deleteRecursive(String path)
- 读取节点
- 读取子节点的相对路径列表
public List getChildren(String path)
- 获取节点内容
public T readData(String path)
public T readData(String path, boolean returnNullIfPathNotExists)
public T readData(String path, Stat stat)
- 更新数据
public void writeData(String path, Object object)
public void writeData(final String path, Object datat, final int expectedVersion)
public Stat writeDataReturnStat(final String path, Object datat, final int expectedVersion)
- 检测是否存在
protected boolean exists(final String path, final boolean watch)
- 注册监听
接口类 | 注册监听方法 | 解除监听方法 |
IZkChildListener | ZkClient的subscribeChildChanges方法 | ZkClient的unsubscribeChildChanges方法 |
IZkDataListener | ZkClient的subscribeDataChanges方法 | ZkClient的subscribeChildChanges方法 |
IZkStateListener | ZkClient的subscribeStateChanges方法 | ZkClient的unsubscribeStateChanges方法 |
4、zookeeper分布式锁流程图
- 流程总图
- 流程分布说明
应用A访问应用程序需要获取共享锁,首先在zookeeper的永久节点lockRoot下创建临时有序节点“00001”,
创建成功后获取根节点下的所有子节点,对其进行排序,由于当前是最小节点,进入同步代码中执行,加锁成功。如下图:
应用B访问应用程序需要获取共享锁,首先在zookeeper的永久节点lockRoot下创建临时有序节点“00002”,
创建成功后获取根节点下的所有子节点,对其进行排序。此时节点“00002”不是最小节点,无法进入同步代码块中,线程阻塞。
应用B无法获取到锁以后,在获取到的节点中取其前一个节点“00001”,监听其节点变化。
应用程序A执行完业务以后,删除临时节点“00001”,此时应用程序B监听节点“00001”,应用B收到通知后,
重新获取节点lockRoot下面的节点信息。 此时应用B创建的节点“00002"为最小节点,将获取锁,将阻塞的线程唤醒,执行同步代码;
5、zookeeper分布式锁代码实现
- 初始化zookeeper客户端,创建监听节点
//zookeeper 服务地址
private String server = "127.0.0.1:2181";
//客户端
private ZkClient zkClient;
//永久根节点
private static final String rootPath = "/zookeeperLock";
public ZookeeperLock() {
zkClient = new ZkClient(server, 5000, 20000);
buildRoot();
}
// 构建根节点
public void buildRoot() {
if (!zkClient.exists(rootPath)) {
zkClient.createPersistent(rootPath);
}
}
- 创建临时有序节点
public Lock createLockNode(String lockId) {
String nodePath = zkClient.createEphemeralSequential(rootPath + "/" + lockId, "w");
return new Lock(lockId, nodePath);
}
- 激活锁:即获取根节点下面的所有子节点,进行排序获取最小节点有当前节点的值是否一致,如一致则激活成功,否则进入等待中,同时监听当前节点的前一节点。
// 获取锁
public Lock lock(String lockId, long timeout) {
// 创建临时节点
Lock lockNode = createLockNode(lockId);
lockNode = tryActiveLock(lockNode);// 尝试激活锁
if (!lockNode.isActive()) {
try {
synchronized (lockNode) {
lockNode.wait(timeout); // 线程锁住
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
if (!lockNode.isActive()) {
throw new RuntimeException(" lock timeout");
}
return lockNode;
}
// 尝试激活锁
private Lock tryActiveLock(Lock lockNode) {
// 获取根节点下面所有的子节点
List<String> list = zkClient.getChildren(rootPath)
.stream()
.sorted()
.map(p -> rootPath + "/" + p)
.collect(Collectors.toList()); // 判断当前是否为最小节点
String firstNodePath = list.get(0);
// 最小节点是不是当前节点
if (firstNodePath.equals(lockNode.getPath())) {
lockNode.setActive(true);
} else {
String upNodePath = list.get(list.indexOf(lockNode.getPath()) - 1);
zkClient.subscribeDataChanges(upNodePath, new IZkDataListener() {
@Override
public void handleDataChange(String dataPath, Object data) throws Exception {
}
@Override
public void handleDataDeleted(String dataPath) throws Exception {
// 事件处理 与心跳 在同一个线程,如果Debug时占用太多时间,将导致本节点被删除,从而影响锁逻辑。
System.out.println("" + dataPath);
//重新尝试获取锁
Lock lock = tryActiveLock(lockNode);
synchronized (lockNode) {
if (lock.isActive()) {
lockNode.notify(); // 释放了
}
}
//解除监听
zkClient.unsubscribeDataChanges(upNodePath, this);
}
});
}
return lockNode;
}
- 释放锁
// 释放锁
public void unlock(Lock lock) {
if (lock.isActive()) {
//删除临时节点
zkClient.delete(lock.getPath());
}
}
- 实体类Lock
public class Lock {
private String lockId;
private String path;
private boolean active;
public Lock(String lockId, String path) {
this.lockId = lockId;
this.path = path;
}
public Lock() {
}
public String getLockId() {
return lockId;
}
public void setLockId(String lockId) {
this.lockId = lockId;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
}
6、总结
- client调用create()方法创建“/root/lock_”节点,注意节点类型是EPHEMERAL_SEQUENTIAL。
- client调用getChildren("/root/lock_")来获取所有已经创建的子节点,这里并不注册任何Watcher。
- 客户端获取到所有子节点Path后,如果发现自己在步骤1中创建的节点是所有节点中最小的,那么就认为这个客户端获得了锁。
- 如果在步骤3中,发现不是最小的,那么找到比自己小的那个节点,然后对其调用subscribeDataChanges方法注册事件监听。
- 之后一旦这个被关注的节点移除,客户端会收到相应的通知,这个时候客户端需要再次调用getChildren("/root/lock_")来确保自己是最小的节点,然后进入步骤3。