zookeeper快速入门.分布式读写锁

概述

分布式读写锁地址.
zookeeper 是基于 观察者模式 设计的分布式服务管理框架,负责存储大家都关心的数据,接受观察者的注册,一旦这些数据发生变化,就通知注册的观察者

特点

  1. ZK集群: 一个leader 和多个follower 组成
  2. 集群中只要有半数以上的节点存活,zk就能对外提供服务
  3. 全局数据一致,无论哪个节点,数据都是一致的。
  4. 更新请求顺序执行,来自同一个client的请求按照发送顺序执行
  5. 更新的原子性,更新请求要么成功,要么失败
  6. 一定时间范围内,client能读到最新数据

数据结构

在这里插入图片描述
Zookeeper 数据模型整体可以看成是一个树,每个节点称为一个ZNode . 每个ZNode 默认能够存储 1MB数据,每个ZNode 都可以 通过其路径唯一标识

应用场景

统一命名服务,统一配置管理,统一集群管理,服务器节点动态上下线,软负载均衡

安装

单机版

  1. zookeeper下载地址. 下载并上传到linux
  2. 修改配置文件 zoo_sample.cfgzoo.cfg
  3. 修改 zoo.cfg 下的 dataDir=/data/zookeeper/
  4. 相应位置创建文件夹
  5. ./zkServer.sh start 启动 zookeeper
  6. ./zkCli.sh 启动zookeeper 客户端

分布式版本

  1. 拷贝zk到其他服务器上
  2. 在 dataDir 的地方创建一个 vim myid 填写 服务器id 2,3,4 之类的
  3. 修改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挂了,需要另一个端口来进行选举,是选举的端口

  1. 启动zk

配置参数

tickTime = 2000 通信心跳数,客户端和服务端心跳时间,毫秒
initLimit=10 初始通信时限 initLimix * tickTime 超过这个时间,leader 认为 follower 已经死了
syncLimit = 5 启动之后的leader 和follower 的时限

节点类型

持久性

  1. 客户端和服务器断开连接后,创建的节点不删除
  2. 持久化还有顺序目录节点,ZNode 后面还会附带单调递增的计数器,由父节点维护.
    短暂
  3. 断开连接后就被删除
  4. 临时节点带顺序的
    客户端和服务器断开连接后,创建的节点自己删除

客户端shell命令

  1. ls path ZNode 信息 例如 ls / 查看 根节点下的ZNode 默认 有一个zookeeper
  2. create /path/ZNode value 创建一个节点 不能跨级创建 create /school tom 根节点下创建了一个school节点,节点的值是tom
  3. get /path/node 获得节点内的值
  4. create -e /node 创建短暂的节点,当 客户端断开连接后,该节点消失
  5. create -s /node 获得后面带有计数器的节点,本来同名的节点是不能创建,但是-s之后创建的名称后面会有序号
  6. set /path/node value 给某个节点设置值
  7. get -w /path/node 监听某个节点的变化,当发生变化的时候会收到通知,只有一次
  8. ls -w /path 监听某个路径下变化,该路径下节点发生变化接受通知,子节点变化不会有反应
  9. delete /node 删除节点
    10.deleteall /path 递归删除

watch,事件监听机制

特性说明
一次性watcher 是一次性的,收到了通知之后需要重新注册
客户端顺序回调不知道什么意思
轻量级回调参数里面只包含通知状态,时间类型和节点的路径。
时效性watch在session失效的时候彻底失效

watcher 接口设计

任何实现了watcher 接口的都是一个新的watcher . watcher 内部包含了两个枚举类型 KeeperStateEventType. 在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数组没有节点,并且自己前面没有节点.如果有写锁,则监听写锁的最后一个节点的消失.消失之后在重新尝试获取锁.
如果当前是写锁,则只需要判断自己是不是写锁数组里面的第一个,不是的话,就监听前一个写锁的消失

代码

github代码.

  1. 引入pom文件
<!-- https://mvnrepository.com/artifact/org.apache.zookeeper/zookeeper -->
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.6.1</version>
</dependency>
  1. 代码实现
    == 在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

  1. 引入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>
  1. 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();
  1. 重连策略 (有待测试)
    new RetryOneTime(3000) 断开链接后3秒重连一次
    new RetryNTimes(50,3000) 每3秒连接一次,一共连接50次
    new RetryUntilElapsed(10000,3000) 每3秒重联一次,总共10秒后断开连接
    测试一次连接策略的时候,连接时长超过了3秒连接还是可以连接的上
  2. 创建节点
client.create()
                .withMode(CreateMode.PERSISTENT)
                .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
                .forPath("/node01");
  1. 递归创建节点(当父节点不存在的时候创建父节点)
 client.create()
                .creatingParentsIfNeeded()
                .withMode(CreateMode.PERSISTENT)
                .withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
                .forPath("/parent/sonnode");
  1. 删除节点
 client.delete()
                .deletingChildrenIfNeeded()
                .forPath("/node");
  1. 判断节点是否存在
client.checkExists().forPath("/dds");
  1. 监听节点的创建
    节点的创建和改变都会触发回调
    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;
    }


}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值