zoukankan      html  css  js  c++  java
  • 分布式协调组件Zookeeper实践之 服务器动态上下线 和分布式锁实现

    前奏知识:参考上一篇:分布式协调组件 Zookeeper,选举机制与ZAB协议

      ZooKeeper是⼀个典型的发布/订阅模式的分布式数据管理与协调框架,我们可以使⽤它来进⾏分布式 数据的发布与订阅。另⼀⽅⾯,通过对ZooKeeper中丰富的数据节点类型进⾏交叉使⽤,配合Watcher 事件通知机制,可以⾮常⽅便地构建⼀系列分布式应⽤中都会涉及的核⼼功能,如数据发布/订阅、命名 服务、集群管理、Master选举、分布式锁和分布式队列等。那接下来就针对这些典型的分布式应⽤场景 来做下介绍。

    Zookeeper的两⼤特性:

    • 1.客户端如果对Zookeeper的数据节点注册Watcher监听,那么当该数据节点的内容或是其⼦节点 列表发⽣变更时,Zookeeper服务器就会向订阅的客户端发送变更通知。   
    • 2.对在Zookeeper上创建的临时节点,⼀旦客户端与服务器之间的会话失效,那么临时节点也会被 ⾃动删除

      利⽤其两⼤特性,可以实现集群机器存活监控系统,若监控系统在/clusterServers节点上注册⼀个 Watcher监听,那么但凡进⾏动态添加机器的操作,就会在/clusterServers节点下创建⼀个临时节 点:/clusterServers/[Hostname],这样,监控系统就能够实时监测机器的变动情况。

    服务器动态上下线监听:

      分布式系统中,主节点会有多台,主节点可能因为任何原因出现宕机或者下线,⽽任意⼀台客户端都要 能实时感知到主节点服务器的上下线。

    思路分析:

    具体实现:

    服务端:

    // 1 连接ZK
    // 2  创建临时顺序节点,数据内容写ip和端口
    // 3 创建时间服务线程
    
    import org.I0Itec.zkclient.ZkClient;
    
    //服务端主要提供了client需要的一个时间查询服务,服务端向zk建立临时节点
    public class Server {
    
        //获取zkclient
        ZkClient zkClient = null;
    
        private void connectZk() {
            // 创建zkclient
            zkClient = new ZkClient("linux121:2181,linux122:2181");
            //创建服务端建立临时节点的目录
            if (!zkClient.exists("/servers")) {
                zkClient.createPersistent("/servers");
            }
        }
    
        //告知zk服务器相关信息
        private void saveServerInfo(String ip, String port) {
            final String sequencePath = zkClient.createEphemeralSequential("/servers/server", ip + ":" + port);
            System.out.println("----->>> ,服务器:" + ip + ":" + port + ",向zk保存信息成功,成功上线可以接受client查询");
        }
    
        public static void main(String[] args) {
            //准备两个服务端启动上线(多线程模拟,一个线程代表一个服务器)
            final Server server = new Server();
            server.connectZk();
            server.saveServerInfo(args[0], args[1]);
            //提供时间服务的线程没有启动,创建一个线程类,可以接收socket请求
            new TimeService(Integer.parseInt(args[1])).start();
        }
    }

     服务端提供时间查询的线程类:

    import java.io.IOException;
    import java.io.OutputStream;
    import java.net.ServerSocket;
    import java.net.Socket;
    import java.util.Date;
    
    //提供时间查询服务
    public class TimeService extends Thread {
        private int port = 0;
    
        public TimeService(int port) {
            this.port = port;
        }
    
        @Override
        public void run() {
            //通过socket与client进行交流,启动serversocket监听请求
            try {
                //指定监听的端口
                final ServerSocket serverSocket = new ServerSocket(port);
    
                //保证服务端一直运行
                while (true) {
                    final Socket socket = serverSocket.accept();
                    //不关心client发送内容,server只考虑发送一个时间值
                    final OutputStream out = socket.getOutputStream();
                    out.write(new Date().toString().getBytes());
                }
    
            } catch (IOException e) {
                e.printStackTrace();
            }
    
        }
    }

    client端:

    import org.I0Itec.zkclient.IZkChildListener;
    import org.I0Itec.zkclient.ZkClient;
    
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.OutputStream;
    import java.net.Socket;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Random;
    
    // 注册监听zk指定目录,
    //维护自己本地一个servers信息,收到通知要进行更新
    //发送时间查询请求并接受服务端返回的数据
    public class Client {
        //获取zkclient
        ZkClient zkClient = null;
    
        //维护一个serversi 信息集合
        ArrayList<String> infos = new ArrayList<String>();
    
        private void connectZk() {
            // 创建zkclient
            zkClient = new ZkClient("linux121:2181,linux122:2181");
            //第一次获取服务器信息,所有的子节点
            final List<String> childs = zkClient.getChildren("/servers");
            for (String child : childs) {
                //存储着ip+port
                final Object o = zkClient.readData("/servers/" + child);
                infos.add(String.valueOf(o));
            }
    
            //对servers目录进行监听
            zkClient.subscribeChildChanges("/servers", new IZkChildListener() {
                public void handleChildChange(String s, List<String> children) throws Exception {
                    //接收到通知,说明节点发生了变化,client需要更新infos集合中的数据
                    ArrayList<String> list = new ArrayList<String>();
                    //遍历更新过后的所有节点信息
                    for (String path : children) {
                        final Object o = zkClient.readData("/servers/" + path);
                        list.add(String.valueOf(o));
                    }
    
                    //最新数据覆盖老数据
                    infos = list;
                    System.out.println("--》接收到通知,最新服务器信息为:" + infos);
                }
            });
        }
    
    
        //发送时间查询的请求
        public void sendRequest() throws IOException {
            //目标服务器地址
            final Random random = new Random();
            final int i = random.nextInt(infos.size()); //随机选择一个服务器
            final String ipPort = infos.get(i);
            final String[] arr = ipPort.split(":");
    
            //建立socket连接
    
            final Socket socket = new Socket(arr[0], Integer.parseInt(arr[1]));
            final OutputStream out = socket.getOutputStream();
            final InputStream in = socket.getInputStream();
            //发送数据
            out.write("query time".getBytes());
            out.flush();
            //接收返回结果
            final byte[] b = new byte[1024];
            in.read(b);//读取服务端返回数据
            System.out.println("client端接收到server:+" + ipPort + "+返回结果:" + new String(b));
    
    
            //释放资源
            in.close();
            out.close();
            socket.close();
        }
    
    
        public static void main(String[] args) throws InterruptedException {
    
            final Client client = new Client();
            client.connectZk();   //监听器逻辑
            while (true) {
                try {
                    client.sendRequest(); //发送请求
                } catch (IOException e) {
                    e.printStackTrace();
                    try {
                        client.sendRequest();
                    } catch (IOException e1) {
                        e1.printStackTrace();
                    }
                }
                //每隔几秒中发送一次请求到服务端
                Thread.sleep(2000);
            }
        }
    }

    分布式锁:

    1.什么是锁:

    • 在单机程序中,当存在多个线程可以同时改变某个变量(可变共享变量)时,为了保证线程安全 (数据不能出现脏数据)就需要对变量或代码块做同步,使其在修改这种变量时能够串⾏执⾏消除并 发修改变量。
    • 对变量或者堆代码码块做同步本质上就是加锁。⽬的就是实现多个线程在⼀个时刻同⼀个代码块只 能有⼀个线程可执⾏

     2. 分布式锁

      分布式的环境中会不会出现脏数据的情况呢?类似单机程序中线程安全的问题。观察下⾯的例⼦:

      

      上⾯的设计是存在线程安全问题。

    问题:

      假设Redis ⾥⾯的某个商品库存为 1;此时两个⽤户同时下单,其中⼀个下单请求执⾏到第 3 步,更新 数据库的库存为 0,但是第 4 步还没有执⾏。

    ⽽另外⼀个⽤户下单执⾏到了第 2 步,发现库存还是 1,就继续执⾏第 3 步。但是商品库存已经为0, 所以如果数据库没有限制就会出现超卖的问题。

    解决方案:

      

    公司业务发展迅速,系统应对并发不断提⾼,解决⽅案是要增加⼀台机器,结果会出现更⼤的问题:

      假设有两个下单请求同时到来,分别由两个机器执⾏,那么这两个请求是可以同时执⾏了,依然存在超 卖的问题。

      因为如图所示系统是运⾏在两个不同的 JVM ⾥⾯,不同的机器上,增加的锁只对⾃⼰当前 JVM ⾥⾯的 线程有效,对于其他 JVM 的线程是⽆效的。所以现在已经不是线程安全问题。需要保证两台机器加的锁 是同⼀个锁,此时分布式锁就能解决该问题。

      分布式锁的作⽤:在整个系统提供⼀个全局、唯⼀的锁,在分布式系统中每个系统在进⾏相关操作的时 候需要获取到该锁,才能执⾏相应操作。

    zk实现分布式锁:

      利⽤Zookeeper可以创建临时带序号节点的特性来实现⼀个分布式锁。

    实现思路:

    • 锁就是zk指定⽬录下序号最⼩的临时序列节点,多个系统的多个线程都要在此⽬录下创建临时的顺 序节点,因为Zk会为我们保证节点的顺序性,所以可以利⽤节点的顺序进⾏锁的判断。
    • 每个线程都是先创建临时顺序节点,然后获取当前⽬录下最⼩的节点(序号),判断最⼩节点是不是 当前节点,如果是那么获取锁成功,如果不是那么获取锁失败。
    • 获取锁失败的线程获取当前节点上⼀个临时顺序节点,并对对此节点进⾏监听,当该节点删除的时 候(上⼀个线程执⾏结束删除或者是掉线zk删除临时节点)这个线程会获取到通知,代表获取到了 锁。

    流程图:

    分布式锁的具体代码实现:

    整体框架:

    //zk实现分布式锁
    public class DisLockTest {
        public static void main(String[] args) {
            //使用10个线程模拟分布式环境
            for (int i = 0; i < 10; i++) {
                new Thread(new DisLockRunnable()).start();//启动线程
            }
        }
    
        static class DisLockRunnable implements Runnable {
    
            public void run() {
                //每个线程具体的任务,每个线程就是抢锁,
                final DisClient client = new DisClient();
                client.getDisLock();
    
                //模拟获取锁之后的其它动作
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //释放锁
                client.deleteLock();
            }
        }
    }

    具体抢锁类:

    import org.I0Itec.zkclient.IZkDataListener;
    import org.I0Itec.zkclient.ZkClient;
    
    import java.util.Collections;
    import java.util.List;
    import java.util.concurrent.CountDownLatch;
    
    //抢锁
    //1. 去zk创建临时序列节点,并获取到序号
    //2. 判断自己创建节点序号是否是当前节点最小序号,如果是则获取锁
    //执行相关操作,最后要释放锁
    //3. 不是最小节点,当前线程需要等待,等待你的前一个序号的节点
    //被删除,然后再次判断自己是否是最小节点。。。
    public class DisClient {
    
        public DisClient() {
            //初始化zk的/distrilocl节点,会出现线程安全问题
            synchronized (DisClient.class){
                if (!zkClient.exists("/distrilock")) {
                    zkClient.createPersistent("/distrilock");
                }
            }
    
        }
    
        //前一个节点
        String beforNodePath;
    
        String currentNoePath; //当前节点
        //获取到zkClient
        private ZkClient zkClient = new ZkClient("linux121:2181,linux122:2181");
        //把抢锁过程为两部分,一部分是创建节点,比较序号,另一部分是等待锁
    
        //完整获取锁方法
        public void getDisLock() {
            //获取到当前线程名称
            final String threadName = Thread.currentThread().getName();
            //首先调用tryGetLock
            if (tryGetLock()) {
                //说明获取到锁
                System.out.println(threadName + ":获取到了锁");
            } else {
                // 没有获取到锁,
                System.out.println(threadName + ":获取锁失败,进入等待状态");
                waitForLock(); //等待锁
                //递归获取锁
                getDisLock();
            }
    
        }
    
        CountDownLatch countDownLatch = null;
    
        //尝试获取锁
        public boolean tryGetLock() {
            //创建临时顺序节点,/distrilock/序号
            if (null == currentNoePath || "".equals(currentNoePath)) {
                currentNoePath = zkClient.createEphemeralSequential("/distrilock/", "lock");
            }
            //获取到/distrilock下所有的子节点
            final List<String> childs = zkClient.getChildren("/distrilock");
            //对节点信息进行排序
            Collections.sort(childs); //默认是升序
            final String minNode = childs.get(0); //最小序号节点
            //判断自己创建节点是否与最小序号一致
            if (currentNoePath.equals("/distrilock/" + minNode)) {
                //说明当前线程创建的就是序号最小节点
                return true;
            } else {
                //说明最小节点不是自己创建,要监控自己当前节点序号前一个的节点
                final int i = Collections.binarySearch(childs, currentNoePath.substring("/distrilock/".length()));
                //前一个(lastNodeChild是不包括父节点)
                String lastNodeChild = childs.get(i - 1);
                beforNodePath = "/distrilock/" + lastNodeChild; //获取前一个节点,并告知获取锁失败
            }
    
            return false;
        }
    
        //等待之前节点释放锁,如何判断锁被释放,需要唤醒线程继续尝试tryGetLock
        public void waitForLock() {
    
            //准备一个监听器
            final IZkDataListener iZkDataListener = new IZkDataListener() {
    
                public void handleDataChange(String s, Object o) throws Exception {
    
                }
    
                //删除
                public void handleDataDeleted(String s) throws Exception {
                    //提醒当前线程再次获取锁
                    countDownLatch.countDown();//把值减1变为0,唤醒之前await线程
                }
            };
            //监控前一个节点
            zkClient.subscribeDataChanges(beforNodePath, iZkDataListener);
    
            //在监听的通知没来之前,该线程应该是等待状态,先判断一次上一个节点是否还存在
            if (zkClient.exists(beforNodePath)) {
                //开始等待,CountDownLatch:线程同步计数器
                countDownLatch = new CountDownLatch(1);
                try {
                    countDownLatch.await();//阻塞,countDownLatch值变为0
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
            //解除监听
            zkClient.unsubscribeDataChanges(beforNodePath, iZkDataListener);
        }
    
    
        //释放锁
        public void deleteLock() {
            if (zkClient != null) {
                zkClient.delete(currentNoePath);
                zkClient.close();
            }
        }
    }

      工业中一般使用redis实现分布式锁,而不是zk,关于redis 实现分布式锁的介绍,将在后续文章中介绍。

      

  • 相关阅读:
    关于idea的目录, mybatis里mapper无法用resource获取 和 驼峰命令规则
    直接调用类方法 和 new再调用方法 的区别
    腾讯笔试题
    linux安装包
    centos 学习笔记一
    putty链接l虚拟机linux centos
    单链表的一般处理(C语言)
    华为2011机试题
    【转】函数返回类型为指针类型时的一些问题
    在 Windows Server 2012 上安装 dotNET Framework v3.5
  • 原文地址:https://www.cnblogs.com/wanghzh/p/15218199.html
Copyright © 2011-2022 走看看