ZooKeeper介绍和实操(增删改查、监听、分布式锁)

        本文需要读者有一定的基础,最后一个分布式锁案例部分需要有AOP、JUC、反射、自定义注解等基础。不适合当面试题,适合实操。

        为什么会使用这些技术进行分布式锁的操作,是因为楼主的公司是这样做的,然后非常的方便,只需要在需要的地方(类、方法)添加注解就能实现分布式锁,小伙伴们跟着楼主一起敲一下命令和代码,就能学会Zookeeper的相关知识点。

        Lets go!

        再等下!不会有人SpringMVC还没学就来看Zookeeper吧,凝视 .jpg,岂不跟我一样是天才少年?冲!

        最后等下!本文没有介绍Zookeeper的下载和安装,博主是使用的zookeeper3.5.10,并且是在windows环境下使用的。给你们推荐个安装Zookeeper的网址:ZooKeeper下载和配置

目录

zookeeper是什么

zookeeper的数据结构

zk的节点类型:

Zookeeper客户端命令(CURD)

Zookeeper JavaAPI 操作(Curator)

Curator介绍

Curator的使用

Curator创建节点

Curator查询节点

Curator修改数据

Curator删除节点

Watch事件监听

分布式锁

ZooKeeper分布式锁原理:

Curator实现分布式锁的API:

Curator使用分布式可重入锁的案例:

Zookeeper整合自定义注解实现分布式锁(了解)

一、定义一个是临界资源的bean(Ticket)表示所有的票都在这里出,这个bean是单例的;

二、在configuration里面进行配置好InterProcessMutex

三、自定义一个注解

四、定义一个用来加锁的bean,会在service层调用它

五、定义好正常的Controller层和Service层

六、测试效果

Zookeeper结合AOP和自定义注解实现分布式锁(重要)

一、改变上面例子的service

二、引入AOP所需要的依赖

三、定义切片类,切入点,通知

四、测试效果


zookeeper是什么

ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,主要为了解决分布式架构下数据一致性问题,典型的应用场景有分布式配置中心、分布式注册中心、分布式锁、分布式队列、集群选举、分布式屏障、发布/订阅等场景。

zookeeper的数据结构

Zookeeper是一个类似于文件系统的数据结构,最外层我们可以想象成一个大的文件夹,里面都是一些小的文件夹。可以跟Linux的文件路径进行联想。

 

zk的节点类型:

  1. 持久化目录节点: 客户端与zookeeper断开连接后,该节点依旧存在,只要不手动删除该节点,他将永远存在。

  2. 持久化顺序编号目录节点:-s 客户端与zookeeper断开连接后,该节点依旧存在,只是zookeeper给该节点名称进行顺序编号。

  3. 临时目录节点:-e 客户端与zookeeper断开连接后,该节点被删除。

  4. 临时顺序编号目录节点:-se 客户端与zookeeper断开连接后,该节点被删除,只是zookeeper给该节点名称进行顺序编号。

Zookeeper客户端命令(CURD)

  • ls [path]:查看某个节点下的节点

  • ls -s [path]:查看某个节点的详细信息

  • create [path]:在某个节点下创建节点

  • create [path] [value]:在某个节点下创建节点(带数据)

  • get [path]:获取某个节点的数据

  • set [path]:设置/修改某个节点的数据

  • delete [path]:删除某个节点

  • 节点不能重复创建

  • deleteall [path]:删除节点及其子节点

  • help:查看zk的命令

以下是演示过程,根节点用反斜杠来代表 /

  • 创建临时节点:create -e [path]

    使用参数 -e来指定为临时节点

  • 创建顺序节点:create -s [path]

    使用参数 -s来指定顺序节点

  • 创建临时顺序节点:create -es [path]

Zookeeper JavaAPI 操作(Curator)

常见的Zookeeper的API:

  • 原生Java API:官方提供,不好用,写代码麻烦

  • ZKClient:在原生API上面做了一些封装,依旧很麻烦

  • Curator:简化了上面的操作

Curator介绍

Curator是Netflix公司开源的一套Zookeeper客户端框架。了解过Zookeeper原生API都会清楚其复杂度。Curator帮助我们在其基础上进行封装、实现一些开发细节,包括接连重连、反复注册Watcher和NodeExistsException等。目前已经作为Apache的顶级项目出现,是最流行的Zookeeper客户端之一。

Curator的使用

  1. 添加依赖:如果zookeeper是3.5.x以上的,就要用curator的4.0.0以上的;

    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-recipes</artifactId>
        <version>4.0.0</version>
    </dependency>
  2. 在配置类中返回类型为CuratorFramework的bean,创建CuratorFramework的方式有两种:

    • 第一种,通过工厂创建

      /**
          connectString – 连接字符串,zk的地址和端口:localhost:2181
          sessionTimeoutMs – 会话超时时间 ms
          connectionTimeoutMs – 连接超时时间 ms
          retryPolicy – 重试策略
           */
          @Bean
          public CuratorFramework curatorFramework(){
              CuratorFramework client = CuratorFrameworkFactory.newClient(
                      "localhost:2181",
                      60*1000,
                      15*1000,
                      new ExponentialBackoffRetry(3000, 10)
              );
              return client;
          }

    • 第二种(推荐),使用build创建:可以指定命名空间,指定命名空间实际上就是指定一个根目录,所有的操作都会在这个目录下,达到逻辑隔离的目的。

          @Bean
          public CuratorFramework curatorFramework(){
              CuratorFramework client = CuratorFrameworkFactory.builder()
                      .connectString("localhost:2181")
                      .sessionTimeoutMs(60 * 1000)
                      .connectionTimeoutMs(15 * 1000)
                      .retryPolicy(new ExponentialBackoffRetry(3000, 10))
                      .namespace("wangpeng")//命名空间,所有操作都在这个目录下进行
                      .build();
              return client;
          }

  3. 在需要的地方自动注入,并且调用start()方法,就能通过curator操作zookeeper啦

Curator创建节点

1.基本创建

2.创建节点带有数据

3.设置节点的类型

4.创建多级节点

@Test
public void createTest1() throws Exception {
    client.start();
    //如果创建节点,没有指定数据,则将当前客户端的ip指定为数据
    String path = client.create().forPath("/app1");
    System.out.println(path);
}

@Test
public void createTest2() throws Exception{
    client.start();
    //创建节点带有数据
    String path = client.create().forPath("/app2","hehe".getBytes(StandardCharsets.UTF_8));
    System.out.println(path);
}

@Test
public void createTest3() throws Exception{
    client.start();
    //设置节点类型:使用.withMode的方式,里面传入一个枚举类型
    //默认类型:持久化的
    String path = client.create().withMode(CreateMode.EPHEMERAL).forPath("/app3");
    System.out.println(path);
}
@Test
public void createTest4() throws Exception{
    client.start();
    //创建多级节点
    //creatingParentsIfNeeded()如果需要就创建父节点
    String path = client.create().creatingParentsIfNeeded().forPath("/app4/p1");
    System.out.println(path);
}

Curator查询节点

1.查询数据:get

2.查询子节点:ls

3.查询节点状态信息 ls -s

@Test
public void testGet1() throws Exception {
    client.start();
    //查询数据:get
    byte[] data = client.getData().forPath("/app1");
    System.out.println(new String(data));
}
@Test
public void testGet2() throws Exception {
    client.start();
    //查询子节点:ls
    List<String> strings = client.getChildren().forPath("/");
    strings.forEach(s -> System.out.println(s));
}
@Test
public void testGet3() throws Exception {
    client.start();
    //查询节点状态信息 ls -s
    //创建一个状态的对象
    Stat status = new Stat();
​
    client.getData().storingStatIn(status).forPath("/app1");
    System.out.println(status);
}

Curator修改数据

1.修改数据:setData().forPath()

2.根据版本修改(乐观锁):setData().withVersion(version).forPath(),乐观锁就不展开讲了,有兴趣的同学可以自己去了解

@Test
    public void testSet() throws Exception {
        client.start();
//        1.修改数据
        client.setData().forPath("/app1","ppp".getBytes(StandardCharsets.UTF_8));
    }
@Test
    public void testSetForVersion() throws Exception {
        client.start();
//        2.根据版本修改(乐观锁)
        Stat status = new Stat();
        client.getData().storingStatIn(status).forPath("/app1");
​
        int version = status.getVersion();
​
        client.setData().withVersion(version).forPath("/app1","test".getBytes(StandardCharsets.UTF_8));
    }

Curator删除节点

1.删除单个节点

2.删除带有子节点的节点

3.必须删除成功

4.回调

    @Test
    public void testDelete1() throws Exception {
        client.start();
//        1.删除单个节点
        client.delete().forPath("/app1");
    }
    @Test
    public void testDelete2() throws Exception {
        client.start();
//        2.删除带有子节点的节点
        client.delete().deletingChildrenIfNeeded().forPath("/app4");
    }
    @Test
    public void testDelete3() throws Exception {
        client.start();
//        3.必须删除成功,会一直重试
        client.delete().guaranteed().forPath("/app2");
    }
    @Test
    public void testDelete4() throws Exception {
        client.start();
//        4.回调
        client.delete()
                .guaranteed()
                .inBackground(new BackgroundCallback() {
                    @Override
                    public void processResult(CuratorFramework client, CuratorEvent event) throws Exception {
                        System.out.println("我被删除啦~~");
                        System.out.println(event);
                    }
                })
                .forPath("/app1");
    }

Watch事件监听

zookeeper允许用户在指定节点上注册一些Watcher,并且在特定事件触发的时候,zookeeper服务端会将事件通知到感兴趣的客户端上面去,该机制是zookeeper实现分布式协调服务的重要特性。

zookeeper中引入了Watcher机制来实现发布/订阅功能,能够让多个订阅者同事监听某一个对象,当一个对象自身状态变化时,会通知所有订阅者。

Curator引入了Cache来实现对zookeeper服务的事件的监听。

Curator提供了三种Watcher:

  • NodeCache:只是监听某一个特定的节点(增删改都会监听到)

        @Test
        public void testNodeCache() throws Exception {
    //      1.创建NodeCache对象
            NodeCache nodeCache = new NodeCache(client,"/app1");
    //      2.注册监听
            nodeCache.getListenable().addListener(new NodeCacheListener() {
                @Override
                public void nodeChanged() throws Exception {
                    System.out.println("节点变化了");
    ​
                    //获取节点更改后的数据
                    byte[] data = nodeCache.getCurrentData().getData();
                    System.out.println(new String(data));
                }
            });
    //      3.开启监听:如果设置为true则开启监听是加载缓冲数据
            nodeCache.start(true);
    ​
            //这个while是为了测试的时候,不让进程终止
            while (true) {
    ​
            }
        }

  • PathChildrenCache:监控某一个ZNode节点的子节点,不包括本身

        @Test
        public void testPathChildrenCache() throws Exception {
            //创建监听对象
            PathChildrenCache pathChildrenCache = new PathChildrenCache(client, "/app2", true);
    ​
            //绑定监听器
            pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() {
                @Override
                public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent event) throws Exception {
                    System.out.println("子节点变化了");
                    System.out.println(event);
    ​
                    PathChildrenCacheEvent.Type type = event.getType();
                    if (type.equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)) {
                        byte[] data = event.getData().getData();
                        System.out.println("修改后的数据是:"+new String(data));
                    }
    ​
                }
            });
            //开启监听
            pathChildrenCache.start();
            while (true) {
    ​
            }
        }

  • TreeCache:可以监控整个树上的所有节点,类似于上面两种的结合

    @Test
    public void testTreeCache() throws Exception {
        //1创建监听器
        TreeCache treeCache = new TreeCache(client, "/");
        //2注册监听
        treeCache.getListenable().addListener(new TreeCacheListener() {
            @Override
            public void childEvent(CuratorFramework curatorFramework, TreeCacheEvent event) throws Exception {
                System.out.println("子节点变化了");
                System.out.println(event);
    ​
                TreeCacheEvent.Type type = event.getType();
                if (type.equals(TreeCacheEvent.Type.NODE_UPDATED)) {
                    byte[] data = event.getData().getData();
                    System.out.println("修改后的数据是:"+new String(data));
                }
            }
        });
        //3开启
        treeCache.start();
        while (true) {
    ​
        }
    }

分布式锁

当我们的应用是分布式集群工作环境下,属于多JVM下的工作环境,跨JVM之间已经无法通过多线程的锁解决同步问题。

分布式锁是:处理跨机器的进程之间的数据同步问题

分布式锁实现:

  • 基于缓存的方式:redis、MemCache

  • zookeeper实现分布式锁

  • 数据库层面实现分布式锁:悲观锁、乐观锁

ZooKeeper分布式锁原理:

核心思想:当客户端要获取锁,则创建节点,使用完锁,则删除节点

  1. 客户端获取锁时,在lock节点下创建临时顺序节点

  2. 然后所有客户端都获取lock下面的所有子节点,客户端获取到子节点之后,如果发现自己创建的子节点序号最小,那么就认为该客户端获取到了锁。使用完锁之后,将该节点删除。

  3. 如果发现自己创建的节点并非lock所有子节点中最小的,说明自己没有获得到锁,此时客户端需要找到比自己小的哪个节点,同时注册事件监听器,监听删除事件。(只找一个比自己小的节点,例如:2找1,3找2

  4. 如果发现比自己小的哪个节点被删除,则客户端的Watcher会收到相应通知,此时再判断自己创建的节点是否是lock子节点中序号最小的,如果是则获取到了锁,如果不是则重复3步骤

Curator实现分布式锁的API:

类型
分布式排它锁(非可重入锁)InterProcessSemaphoreMutex
分布式可重入排它锁InterProcessMutex
分布式读写锁InterProcessReadWriteLock
将多个锁作为单个实体容器管理的容InterProcessMultiLock
共享信号量InterProcessSemaphoreV2

Curator使用分布式可重入锁的案例:

使用买票的场景来模拟分布式锁的应用,有一个出票的机构,然后有两个不同服务器的买票机构(用两个线程模拟),然后在出票的地方加上分布式锁;

首先Ticket代表出票的机构,然后两个现场分别在他的run方法里面进行卖票;

首先是Ticket类的实现,定义两个变量:tickets代表剩余票的数量、lock是InterProcessMutex(分布式可重入锁)的引用;

然后是锁的初始化,在无参构造函数里面,进行初始化:首先使用Curator连接到Zookeeper,然后使用client传入锁的构造函数;

然后就是在操作临界资源之前使用acquire()方法获得锁,之后使用release()方法释放锁;

public class Ticket implements Runnable {
​
    private int tickets = 10;
​
    private InterProcessMutex lock;
​
    public Ticket() {
​
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString("localhost:2181")
                .sessionTimeoutMs(60 * 1000)
                .connectionTimeoutMs(15 * 1000)
                .retryPolicy(new ExponentialBackoffRetry(3000, 10))
                .build();
        client.start();
​
        lock = new InterProcessMutex(client,"/lock");
    }
    @Override
    public void run() {
        try {
            //获取锁,参数是时间,代表等等锁的时间
            lock.acquire(3, TimeUnit.SECONDS);
            while (tickets>0){
                System.out.println(Thread.currentThread().getName()+"买了一张票"+tickets--);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                lock.release();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

Zookeeper整合自定义注解实现分布式锁(了解)

还是卖票的案例,将在web的环境下实现;主要目的是帮助楼主回想反射和自定义注解的知识点,有点忘了,尴尬.jpg

一、定义一个是临界资源的bean(Ticket)表示所有的票都在这里出,这个bean是单例的;

@Component
public class Ticket {
​
    private int tickets;
​
    //初始化票的数量
    @PostConstruct
    private void init(){
        this.tickets = 10;
    }
​
    public String saleTicket() {
        return tickets>0?"已经售出票,剩余数量:" + --this.tickets:"没有余票了";
    }
    public String get(){
        return "剩余票数是:"+this.tickets;
    }
}

二、在configuration里面进行配置好InterProcessMutex

    @Bean
    public CuratorFramework curatorFramework(){
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString("localhost:2181")
                .sessionTimeoutMs(60 * 1000)
                .connectionTimeoutMs(15 * 1000)
                .retryPolicy(new ExponentialBackoffRetry(3000, 10))
//                .namespace("lock")//命名空间,所有操作都在这个目录下进行
                .build();
        return client;
    }
​
    @Bean
    public InterProcessMutex interProcessMutex(CuratorFramework curatorFramework){
        curatorFramework.start();
        InterProcessMutex interProcessMutex = new InterProcessMutex(curatorFramework, "/lock");
        return interProcessMutex;
    }

三、自定义一个注解

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
​
}

四、定义一个用来加锁的bean,会在service层调用它

自动注入:临界资源(Ticket)、锁(InterProcessMutex)

@Component
public class TicketLock {
    @Autowired
    Ticket ticket;
​
    @Autowired
    InterProcessMutex lock;
    //逻辑是:传进来service层的类名,通过类名获得类模板
    //然后判断是否被自定义注解标记,如果标记了,则上锁;如果没标记,则不能修改临界资源
    public String sale(String className){
        String result = "加锁失败,无法购票";
        try {
            Class<?> cls = Class.forName(className);
            if (cls.isAnnotationPresent(MyAnnotation.class)) {
                lock.acquire();
                result = ticket.saleTicket();
                lock.release();
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }
}

五、定义好正常的Controller层和Service层

controller

@RestController
@RequestMapping("/ticket")
public class TicketController {
​
    @Autowired
    TicketService service;
​
    @RequestMapping("/sale")
    public String sale(){
        return service.sale();
    }
​
    @RequestMapping("/get")
    public String get(){
        return service.get();
    }
}

service

@Service
public class TicketServiceImpl implements TicketService {
​
    @Autowired
    TicketLock lock;
​
    @Override
    public String sale() {
        String name = this.getClass().getName();
        System.out.println(name);
        return lock.sale(name);
    }
}

六、测试效果

  1. service添加了@MyAnnotation注解

  2. service没有添加@MyAnnotation注解

Zookeeper结合AOP和自定义注解实现分布式锁(重要)

这是实际开发过程中经常会使用的一种通用的方式,上面那个例子只是用来演示反射和注解的一种实现,没有实际意义

一、改变上面例子的service

因为是是使用AOP的方式,所有service直接改成正常的情况就行了,平时咋写就咋写

@Service
@MyAnnotation
public class TicketServiceImpl implements TicketService {
​
    @Autowired
    Ticket ticket;
​
    @Override
    @MyAnnotation
    public String sale() {
        return ticket.saleTicket();
    }
    @Override
    public String get() {
        return ticket.get();
    }
}

二、引入AOP所需要的依赖

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

三、定义切片类,切入点,通知

  • 在spring容易能扫到的位置定义一个类,加上@Component@Component注解,使成为一个切片类

  • 自动注入Curator的锁:InterProcessMutex(可重入分布式锁)

  • 定义好切入点表达式:这里定义的是工程下Service层的所有函数(无论返回值,参数数量等)

  • 写通知方法,逻辑是:

    • 首先获得类对象,判断类上面是否加了@MyAnnotation注解,如果加了则所有的的函数都会上锁

    • 如果类上面没有加,获得方法的签名(MethodSignature),实例化方法;判断方法上面是否加了@MyAnnotation注解,如果加了则给这个方法上锁。

    • 然后是在finally块判断:锁是否加上了,如果加上了则释放。如果没有加上则证明:该类和该方法都没有被@MyAnnotaion注解,所以直接放行

@Aspect
@Component
public class AspectLock {
​
    @Autowired
    InterProcessMutex interProcessMutex;
​
    @Pointcut("execution(*  com.uyun.curator.Service..*.*(..))")
    public void cutOnClass(){}
​
    @Around("cutOnClass()")
    public Object lockOnClass(ProceedingJoinPoint pjp){
        //定义一个连接点(被AOP的那个方法)的返回值
        Object result = null;
        //获得TicketServiceImpl的类对象
        Class<?> cls = pjp.getTarget().getClass();
        try {
            //判断类是否被@MyAnnotation注解
            if (cls.isAnnotationPresent(MyAnnotation.class)){
                System.out.println("通过注解类上锁了...");
                interProcessMutex.acquire();//上锁
                result = pjp.proceed();//执行方法,获得返回值
            }else {
                //类没有被@MyAnnotation注解的时候,拿到方法的签名:里面包括方法的名字,参数类型等
                MethodSignature signature = (MethodSignature) pjp.getSignature();
                //获得方法的名字,和参数类型
                String name = signature.getName();
                Class[] parameterTypes = signature.getParameterTypes();
                //获得该方法的对象
                Method method = cls.getMethod(name,parameterTypes);
                //判断类是否被@MyAnnotation注解
                if (method.isAnnotationPresent(MyAnnotation.class)) {
                    interProcessMutex.acquire();//上锁
                    System.out.println("通过注解方法上锁了...");
                    result = pjp.proceed();//执行方法,获得返回值
                }
            }
        } catch (Throwable e) {
            e.printStackTrace();
        } finally {
            try {
                //判断是否上锁了,如果上锁了则解锁
                if (interProcessMutex.isAcquiredInThisProcess()){
                    interProcessMutex.release();
                }else {
                    //没有上锁,证明:该方法和类都没有被@MyAnnotation标注,直接执行不继续上锁;
                    System.out.println("没有上锁,放行");
                    result = pjp.proceed();
                }
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
        return result;
    }
}

四、测试效果

  1. service没有添加@MyAnnotation注解,sale()方法上加了

  2. service没有添加@MyAnnotation注解,get()方法也没有添加

  3. service添加@MyAnnotation注解,sale()方法也加了

  4. service添加@MyAnnotation注解,get()方法没有添加

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值