zookeeper简介
zookeeper实际就是一个分布式协调工具
单机zookeeper的安装和启动
- 下载zookeeper的tar包, 并解压;
- 进入zookeeper的conf目录, cp zoo.simple.cfg zoo.cfg
- 使用./bin/zkServer.sh start启动;
- ./bin/zkServer.sh status检查状态, 是否启动成功;
- 使用zkCli.sh登录到zookeeper进行操作;
zookeeper基本操作
create /orderService 1 #创建一个节点
delete /orderService #删除一个节点
get /orderService #获取一个节点的值
create -s /seq/ 1 #创建一个有序节点
create -e /tem 1 #创建一个临时节点(会话结束后会消失)
get /seq true #为节点添加一个watch
set /seq test #更新节点的值
zookeeper的节点特性
- 同级节点具有唯一性(不能再同一级下创建相同的节点);
- 节点从持久化上划分, 分为临时节点和持久化节点, 临时节点会在当次会话结束后删除掉;
- 从有序无序上分, 分为有序节点和无序节点;
- 临时节点下不能存在子节点;
集群搭建
- 在三台机器上分别解压zookeeper, 并复制好配置文件;
- 修改配置文件
server.1=ip1:2888:3888
server.2=ip2:2888:3888
server.3=ip3:2888:3888
- 在dataDir目录下添加myid文件, 内容分别就是1,2,3, 注意要和配置文件中的server对应起来;
节点信息解读
cZxid = 0x34 #事务id
ctime = Tue May 19 17:02:11 CST 2020 # 创建时间
mZxid = 0x34 #更新事务id
mtime = Tue May 19 17:02:11 CST 2020 #更新时间
pZxid = 0x3a #只有子节点列表变更了才会变更_pZxid_,子节点内容变更不会影响_pZxid_
cversion = 6 #跟乐观锁有关, 当前节点子节点的版本号
dataVersion = 0 #数据版本号
aclVersion = 0 #权限版本号
ephemeralOwner = 0x0 #会话信息, 临时节点时会存在值
dataLength = 1 #当前数据长度
numChildren = 6 #子节点数量
zookeeper集群原理
集群角色
- leader: 负责数据的写入和读取, 以及follower节点和leader节点的数据同步
- follower: 负责数据读请求, 如果收到写请求, 会转发给leader节点做处理;
- observer: 不参与投票, 但是可以接收读请求, 设计的初衷是:如果使用大量follower节点提高集群的读请求能力, 那么在提交写请求和集群选举的时候会影响效率, 所以引入observer节点;
数据同步的过程
- leader节点接收到写入数据, 然后发送请求询问follower节点;
- 此时follower节点会返回一个ack, 来告诉leader节点是否可以执行这个事务请求;
- 当有半数节点返回可以提交这个事务请求时, leader节点会发送commit请求给follower节点;
- 最后返回给客户端;
实现数据一致性方式: ZAB协议
- 崩溃恢复
当leader节点宕机或者失去超过半数节点时, 集群会崩溃, 此时就会进入集群和数据恢复阶段, 要保证数据的一致性, 有以下原则
- 保证已经提交的数据不能丢失;
- 被丢弃的消息不能再出现;
ZAB协议为实现上述原则, 引入Zxid和epoch概念
- Zxid(消息的唯一标识, 自增, 并且是64位数据, 高32位保存epoch数据, 低32位是消息的计数器);
- epoch: 朝代的概念, 每次心选举出一个leader, epoch就会加1;
- 原子广播: 就是上面的数据同步机制;
leader选举: 两个方面, 启动时选举, 运行时的崩溃选举
- Zxid最大: 事务id, 事务id越大表示数据越新;
- myid: myid越大, 表示在选举中权重越大;
具体的过程: 投票时会每个节点会先投自己一票, 然后会将自己的Zxid和myid发送给其他节点, 其中, zxid最大的会被选举为leader, 如果zxid一样, 那么就会比较myid, myid大的会被选举为leader;
数据提交的时候, 有的follower不会提交数据, 那客户端怎么保证访问数据的一致;
zookeeper 的读写一致不是在server做的,而是server & client配合的;client 会记录它见过的最大的zxid,读取的时候,如果server发现这条zxid比server端的最大zxid大,则拒绝,client会自动重连到其他server(还在同一个session), 最终会落到有新数据的server上,因为半数已经同意;
源码中的选举过程
zookeeper实现分布式锁
实现代码
public class DistributeLock implements Lock, Watcher {
private ZooKeeper zooKeeper;//zookeeper连接
private String ROOT_LOCK = "/locks"; //定义一个根节点
private String WAIT_LOCK; //需要被监听的节点
private String CURRENT_LOCK; //当前的持有锁的节点
private CountDownLatch countDownLatch;
public DistributeLock() {
try {
this.zooKeeper =
new ZooKeeper("192.168.1.10:2181", 20000, this);
Thread.sleep(10000);
Stat stat = zooKeeper.exists(ROOT_LOCK, false);
if (stat == null) {
//没有则创建
zooKeeper.create(ROOT_LOCK, "0".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
} catch (IOException | InterruptedException | KeeperException e) {
e.printStackTrace();
}
}
@Override
public void lock() {
if (tryLock()) {
System.out.println(
Thread.currentThread().getName() + "->" + CURRENT_LOCK + " 获取锁成功");
return;
}
try {
waitForLock(WAIT_LOCK);
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
}
private boolean waitForLock(String prev) throws KeeperException, InterruptedException {
Stat stat = zooKeeper.exists(ROOT_LOCK+"/"+prev, true);//监听当前节点的上一个节点
if (stat != null) {
System.out.println(
Thread.currentThread().getName() + "-> 等待 " + prev + " 锁释放");
countDownLatch = new CountDownLatch(1); //countDownLatch不能重复使用, 所以在这里创建
countDownLatch.await();
System.out.println(
Thread.currentThread().getName() + "->" + CURRENT_LOCK + " 获取锁成功");
}
return true;
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
try {
//创建临时有序节点
CURRENT_LOCK = zooKeeper.create(ROOT_LOCK + "/", "0".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println(Thread.currentThread().getName() + "->" + CURRENT_LOCK + " 尝试竞争");
//获取根节点下的所有子节点
List<String> childrens = zooKeeper.getChildren(ROOT_LOCK, false);
TreeSet<String> sortSet = new TreeSet<>(childrens);
//获取最小的节点
String firstNode = sortSet.first();
String[] arr = CURRENT_LOCK.split("/");
SortedSet<String> lessThanMe = sortSet.headSet(arr[2]);
//如果最小的节点就是当前创建的节点, 获取锁成功
if (CURRENT_LOCK.equals(ROOT_LOCK + "/" + firstNode)) {
return true;
}
// 不是当前节点, 则将当前节点的最后一个赋值给等待锁
if (!lessThanMe.isEmpty()) {
WAIT_LOCK = lessThanMe.last();
}
} catch (KeeperException | InterruptedException e) {
e.printStackTrace();
}
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
System.out.println(
Thread.currentThread().getName() + "->" + CURRENT_LOCK + " 释放锁");
try {
zooKeeper.delete(CURRENT_LOCK, -1);
zooKeeper.close();
CURRENT_LOCK = null;
} catch (InterruptedException | KeeperException e) {
e.printStackTrace();
}
}
@Override
public Condition newCondition() {
return null;
}
@Override
public void process(WatchedEvent watchedEvent) {
if (countDownLatch != null) { //节点变更触发事件
countDownLatch.countDown();
}
}
}
测试代码
public class DistributeLockTest {
public static void main(String[] args) throws IOException {
CountDownLatch latch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
latch.await();
DistributeLock distributeLock = new DistributeLock();
distributeLock.lock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "Thread-" + i).start();
latch.countDown();
}
System.in.read();
}
}
Watch机制
- watch是一个一次性操作, 即客户端只能监听到一次事件的通知, 所以要通过不断的循环监听达到不断监听的效果;
- zookeeper只有getData, exit和getChildren能绑定事件;
watch事件的类型
- None: 客户端连接状态发生改变时, 触发None事件;
- NodeCreate: 创建节点事件;
- NodeDelete: 节点删除事件;
- NodeDataChange: 节点数据发生变化事件;
- NodeChildChanged: 节点子节点变化事件;
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。