Zookeeper
zookeeper解决的问题
-
zookeeper分布锁
-
zookeeper监控集群
CAP理论:高可用、容错性、一致性最多取其二
BASE理论:B基本可用 、弱状态、最终一致性
zookeeper功能:Dubbo服务治理框架
-
发布订阅:redis mq
-
负载均衡:nginx apche httpd
-
命名服务:
-
分布式协调 通知
-
集群管理 :
-
Master选举:主从结构
-
分布式锁:redis
-
分布式队列
高可用和一致性取舍问题
分布式事务 强一致性:2PC(二阶段提交) 3PC(3阶段提交)
不论是二阶段(强一致性)提交还是三阶段提交 本质是尽可能晚提交(在事务提交前完成其他所有工作)。提交阶段耗时较短出现错误概率低。
]
二阶段缺点:
- 同步阻塞:执行过程中所有节点参与者事务阻塞。譬如使用锁进行隔离性操作时事务没提交占用锁资源并发量很差。
- 单点故障:协调者过于重要,协调者出现故障参与者一直阻塞。即使重新选举也没法解决问题
- 数据不一致:协调者向参与者发送commit请求后,出现网络故障,部分参与者执行commit操作,部分为接收到无法进行事务提交。整个系统出现数据不一致问题。
- 无法确定问题:commit后协调者和参与者同时宕机,此条事务状态不可控。即使重新选出协调者也无法确定事务是否被执行。
- 过于保守:缺乏有效容错机制,任何一个节点的失败会导致整个事务提交失败回滚。
JTA(java里面的分布式事务编程接口规范-JTA) JBOSS(天生支持分布式)
三阶段特点:
- cancommit:询问所有人,根据返回结果决定是否可以执行操作。
- precommit:预执行,如果超时默认执行失败
- docommit:即使超时也会执行
解决了单点故障问题引发问题,减少阻塞发生。根据定时器,没有接收到协调者的信息默认执行commit,可能造成数据不一致问题。(其他参与者可能执行了abort)
高可用性 集群 服务拆分:主从结构->MASTER选举 节点数据不一致
PAXOS算法:zookeeper核心原理 对多个分布式线程数据达成一致的算法
NWR策略 N:数据分片 W:Write 至少由W份数据写入成功 R:Read 至少有R份数据读取成功。 不要求数据强一致性 解决读写均衡的一种方式。Read Only Write All 本质上释放了写模式的压力转给读模式。一般情况要求W+R>N。保证不存在一个数据同时存在读写,一般读写超过N/2就返回结果成功,后续数据同步更新操作由系统后台运行。
ZOOKEEPER保证
- 顺序一致性
- 原子性
- 单一视图
- 可靠性
- 实时性
zookeeper本身就是个分布式系统 单点故障?
一般部署奇数个zk服务器 服务器宕机数量节点总数不变若部署5个3个宕机失败 若6个3个宕机失败 so//
Zab
zk只有一个master 多个slaver
Zab(Zookeeper Atomic Broadcast):解决事务一致性问题zk使用ZAB协议。专门用来设计支持崩溃回复和原子广播的协议。
Zookeeper使用主备模式保证数据一致性:
客户端将信息写入主进程(leader)中然后由leader将数据备份到follower中
复制过程同2PC类似,只要由一般备份返回ACK即可执行提交。
Zookeeper的两种模式
- 消息广播
- 崩溃回复
消息广播
ZAB 协议的消息广播过程使用的是一个原子广播协议,类似一个 二阶段提交过程。对于客户端发送的写请求,全部由 Leader 接收,Leader 将请求封装成一个事务 Proposal,将其发送给所有 Follwer ,然后,根据所有 Follwer 的反馈,如果超过半数成功响应,则执行 commit 操作(先提交自己,再发送 commit 给所有 Follwer)。
注意细节
- Leader 在收到客户端请求之后,会将这个请求封装成一个事务,并给这个事务分配一个全局递增的唯一 ID,称为事务ID(ZXID),ZAB 兮协议需要保证事务的顺序,因此必须将每一个事务按照 ZXID 进行先后排序然后处理。
- 在 Leader 和 Follwer 之间还有一个消息队列,用来解耦他们之间的耦合,解除同步阻塞。HOW?
- zookeeper集群中为保证任何所有进程能够有序的顺序执行,只能是 Leader 服务器接受写请求,即使是 Follower 服务器接受到客户端的请求,也会转发到 Leader 服务器进行处理。
- 实际上,这是一种简化版本的 2PC,不能解决单点问题。等会我们会讲述 ZAB 如何解决单点问题(即 Leader 崩溃问题)。
崩溃恢复
当leader失去与过半的follower联系,进入崩溃回复模式。ZAB协议定义了两个原则:
- ZAB协议确保已经在leader上提交的的事务最终会被服务器所提交。
- ZAB协议确保丢弃那些只在leader上提出/复制。但是没被提交的事务。
解决思路:如果有这样的ZAB选举算法:根据zxid,保证选出来的服务器上有当前zxid最大的事务,那么就能保证当前服务器一定有已经成功提交的事务。
数据同步
崩溃恢复之后,在接受客户端新的请求之气那,leader服务器需要确认事务是否都被过半的follwer提交,即是否完成数据同步。当确认所有的有效follower提交数据后将这些服务器添加到可用服务器列表中。如何做:
通过zxid(服务器处理或是丢弃事务)
在 ZAB 协议的事务编号 ZXID 设计中,ZXID 是一个 64 位的数字,其中低 32 位可以看作是一个简单的递增的计数器,针对客户端的每一个事务请求,Leader 都会产生一个新的事务 Proposal 并对该计数器进行 + 1 操作。
高 32 位代表了每代 Leader 的唯一性,低 32 代表了每代 Leader 中事务的唯一性。同时,也能让 Follwer 通过高 32 位识别不同的 Leader。简化了数据恢复流程。
基于这样的策略:当 Follower 链接上 Leader 之后,Leader 服务器会根据自己服务器上最后被提交的 ZXID 和 Follower 上的 ZXID 进行比对,比对结果要么回滚,要么和 Leader 同步。
Zookeeper分布式锁
实现方式:
- 单节点锁
- 多节点锁
节点种类:
- 持久节点(Persistent),客户端断开连接不删除节点。
- 持久化顺序节点(Persisident_Sequential)znode天生互斥,断开连接后节点存在,zookeeper给该节点名称进行顺序编号。
- 临时节点(Ephemeral),客户端断开连接后该节点被删除
- 临时顺序节点(EPHEMERAL_SEQUENTIAL)客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号
加锁方式:创建节点,解锁方式:删除节点。
如果使用永久节点,当获取节点的客户端宕机,其他客户端不能获取锁,产生死锁。采用临时节点相当于增加了一个失效时间。
单节点锁实现方式
(1)、当一个客户端成功创建一个节点,另外一个客户端是无法创建同名的节点(达到互斥的效果)
(2)、我们注册该节点的listener时,如果节点删除(数据修改),会通知其他的客户端,这个时候其他的客户端可以重新去创建该节点(可以认为时拿到锁的客户端释放锁,其他的客户端可以抢锁)
(3)、创建的节点应该时临时节点,这样保证我们在已经拿到锁的客户端挂掉了会自动释放锁
伪代码:
if(!have childnode){
create node(Ephemeral(临时));
}else {
zkClient.subscribeChildChanges("pth",new zkListener)
}
多节点锁的实现方式(在锁释放时所有线程不会和单点一样去抢占,只要按顺序访问前一个节点就好)
- 某个客户端尝试加锁时,先在该业务节点下,创建一个顺序节点。
- 创建完成后,获取出该业务节点下的所有子节点,并按照按照节点序号排序
- 判断第一位的节点是否为自己的节点,是的话,代表获取锁,执行业务操作
- 不是的话,对排在自己前一位的节点进行监听,客户端挂起
当客户端执行业务完毕后,删除自己的节点,并通知监听自己节点的客户端进行业务操作。
分布式锁代码实现(单节点锁和多节点锁)
抽象类:
import org.I0Itec.zkclient.ZkClient;
/**
* @ClassName AbstractLock
* @Description TODO
* @Author YGuang
* @Date 2019/7/29 17:10
* @Version 1.0
**/
public abstract class AbstractLock {
public static final String ZK_ADDR = "localhost:2181,localhost:2182,localhost:2183";
public static final int SESSION_TIMEOUT = 10000;
//创建ZK
protected ZkClient zkClient=new ZkClient(ZK_ADDR,SESSION_TIMEOUT);
public void getlock(){
String threadName=Thread.currentThread().getName();
if(tryLock()){
System.out.println("获取当前" + threadName+ "锁成功");
}
else {
System.out.println("获取锁失败" +
threadName+"---");
waitLock();
//递归重新获取锁
getlock();
}
}
public abstract boolean tryLock();
public abstract void releaseLock();
public abstract void waitLock();
}
抽象类实现:
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import java.util.concurrent.CountDownLatch;
/**
* @ClassName SimpleZklock
* @Description TODO
* @Author YGuang
* @Date 2019/7/29 17:09
* @Version 1.0
**/
public class SimpleZklock extends AbstractLock {
private static final String NODE_NAME = "/yg";
//计数器 初始化数是线程个数
private CountDownLatch countDownLatch;
@Override
//创建临时节点如果创建成功,获取锁,创建不成功处理异常
public boolean tryLock() {
if (zkClient == null) {
return false;
}
try {
zkClient.createEphemeral(NODE_NAME);
return true;
} catch (Exception e) {
System.out.println(e + "cant get Lock" + "wait---------------");
return false;
}
}
@Override
//释放锁
public void releaseLock() {
if (zkClient != null) {
zkClient.delete(NODE_NAME);
zkClient.close();
System.out.println("releaseLock success----------");
}
}
@Override
//等待锁的过程
public void waitLock() {
//创建监听器
IZkDataListener iZkDataListener = new IZkDataListener() {
@Override
public void handleDataChange(String datapath, Object data) throws Exception {
}
@Override
//节点删除被回调
public void handleDataDeleted(String datapath) throws Exception {
if (countDownLatch != null) {
//给count数减1(默认一个线程已访问?)
countDownLatch.countDown();
}
}
};
zkClient.subscribeDataChanges(NODE_NAME, iZkDataListener);
if (zkClient.exists(NODE_NAME)) {
//如果存在阻塞 设置计时器为1
countDownLatch = new CountDownLatch(1);
try {
//阻塞在当前线程直到所被释放 可能会出现死锁?
countDownLatch.await();
System.out.println("wait lock"+"-------------");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
zkClient.unsubscribeDataChanges(NODE_NAME,iZkDataListener);
}
}
测试:
/**
* @ClassName LockTest
* @Description TODO
* @Author YGuang
* @Date 2019/7/30 10:08
* @Version 1.0
**/
public class LockTest {
public static void main(String[] args) {
for (int i=1;i<10;i++){
new Thread(()->{
AbstractLock zkLock=new SimpleZklock();
zkLock.getlock();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
zkLock.releaseLock();
}).start();
}
}
}
多节点代码:
import org.I0Itec.zkclient.IZkDataListener;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
/**
* @ClassName HighPerformanceLock
* @Description TODO
* @Author YGuang
* @Date 2019/7/30 11:17
* @Version 1.0
**/
public class HighPerformanceLock extends AbstractLock {
public static final String PATH="/highyg";
//当前节点路径
private String currentPath;
//前一个节点路径
private String beforePath;
private CountDownLatch countDownLatch;
public HighPerformanceLock(){
//如果不存在父节点,创建持久节点
if(!zkClient.exists(PATH)){
zkClient.createPersistent(PATH);
}
}
@Override
public boolean tryLock() {
if (null == currentPath || "".equals(currentPath)) {
//在path下创建临时顺序节点将他赋予curretpath 临时节点没必要使用名字 因为临时节点会在一个线程结束后被释放 多个线程访问时会生成多个对应临时节点。
currentPath = zkClient.createEphemeralSequential(PATH + "/", "is a lock");
}
List <String> children = zkClient.getChildren(PATH);
Collections.sort(children);
if (currentPath.equals(PATH + "/" + children.get(0))) {
//如果是第一个即可
return true;
} else {//不是第一个值则获取他前面的节点名称,赋值给beforepath
int pathlength = PATH.length();
String midd=currentPath.substring(pathlength+1);
int index = Collections.binarySearch(children, currentPath.substring(pathlength + 1));
beforePath = PATH + "/" + children.get(index - 1);
}
return false;
}
@Override
public void releaseLock() {
if (null!=zkClient){
//释放节点 删除现在持有节点
zkClient.delete(currentPath);
System.out.println("release current Lock------------");
zkClient.close();
}
}
@Override
public void waitLock() {
IZkDataListener iZkDataListener=new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {
}
@Override
public void handleDataDeleted(String datapath) throws Exception {
if(null!=countDownLatch){
countDownLatch.countDown();
}
}
};
zkClient.subscribeDataChanges(beforePath,iZkDataListener);
if (zkClient.exists(beforePath)){
countDownLatch=new CountDownLatch(1);
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("wai lock ---------");
zkClient.unsubscribeDataChanges(beforePath,iZkDataListener);
}
}
}