zoukankan      html  css  js  c++  java
  • Zk实现分布式锁

    Zookeeper实现分布式锁

    zookeeper实现分布式锁,主要得益于ZooKeeper保证了数据的强一致性这一特性。锁服务可以分为两类,一个是保持独占,另一个是控制时序。

    1. 保持独占,就是所有试图来获取这个锁的客户端,最终只有一个可以成功获得这把锁。通常的做法是把zk上的一个znode看作是一把锁,通过 create znode 的方式来实现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。

    2. 控制时序,就是所有试图来获取这个锁的客户端,最终都是会被安排执行,只是有个全局时序了。做法和上面基本类似,只是这里 /distribute_lock预先已经存在,客户端在它下面创建临时有序节点。Zk 的父节点(/distribute_lock)维持一份 sequence, 保证子节点创建的时序性,从而也形成了每个客户端的全局时序。

    分布式锁的产生的原因:

    1.单纯的Lock锁或者synchronize只能解决单个jvm线程安全问题

    2.分布式 Session 一致性问题

    3.分布式全局id(也可以使用分布式锁) 

    换个角度来说,分布式锁产生的原因就是集群

    在单台服务器上,如何生唯一的订单号,方案有UUid+时间戳方式,redis方式。

    生成订单号, 秒杀抢购时候,首先如果预测是100w订单号,生成放在redis。客户端下单,直接redis去获取即可。因为redis是单线程的,如果实际是150w用户,当redis剩下50w订单号时候,继续生成补充。 

    但是在集群环境下,这种方式其实并不能保证其唯一性。

    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    //生成订单号 时间戳
    public class OrderNumGenerator {
      //区分不同的订单号
        private static int count = 0;
    //单台服务器,多个线程同时生成订单号
        public String getNumber(){
            try {
                Thread.sleep(300);
            } catch (Exception e) {
              
            }
            SimpleDateFormat simpt = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");  
            return simpt.format(new Date()) + "-" + ++count;  //时间戳后面加了 count
        }
    }

    开启100个线程调用:

    public class OrderService implements  Runnable {
    
         private OrderNumGenerator orderNumGenerator  = new OrderNumGenerator(); 
    
         public void run() {
            getNumber();     
        }
    
        public void getNumber(){
        String number =    orderNumGenerator.getNumber();
        System.out.println(Thread.currentThread().getName()+"num"+number);
        }
    
        public static void main(String[] args) {
            OrderService orderService = new OrderService();
            //开启100个线程
            for (int i = 0; i <100; i++) {  
                        new Thread(orderService).start();
            }    
        }
    }

    结果:

     因为多个线程共享同一个全局变量,会产生线程安全问题!

     解决方案当然就是可以加锁:

    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class OrderService implements Runnable {
    
        private OrderNumGenerator orderNumGenerator = new OrderNumGenerator();
        private Lock lock = new ReentrantLock();
    
        public void run() {
            getNumber();
        }
    
        public void getNumber() {
            //加锁
            lock.lock();
            String number = orderNumGenerator.getNumber();
            System.out.println(Thread.currentThread().getName() + "生成订单:" + number);
            //释放锁
            lock.unlock();
        }
    
        public static void main(String[] args) {
            OrderService orderService = new OrderService();
            // 开启100个线程
            for (int i = 0; i < 100; i++) {
                new Thread(orderService).start();
            }
        }
    }

    但是这种方式效率很低!

    如果是集群环境下: 

         每台jvm都有一个count,都有自增的代码操作这个count, 三个不同的jvm独立的用户请 过来 映射到哪个就操作哪个,这时候就产生分布式锁的问题。

    这时候需要分布式锁,共享一个count

    jvm1 操作时候 其他的jvm2 和 jvm3 不可以操作他。

    分布式锁:保证分布式领域中共享数据安全问题,它的实现方式可以有这些:

    1、数据库实现(效率很低)

    2、redis实现(使用redission实现,但是需要考虑释放问题。也比较麻烦)

    3、Zookeeper实现(使用临时节点,效率高,失效时间可以控制)

    4、Spring Cloud实现全局锁(内置的)

    下面用一个业务场景加上代码来说明:

    业务场景

    在分布式情况,生成全局订单号ID

    产生问题

    在分布式集群环境下,每台机器不能实现同步,在分布式场景下使用时间戳生成订单号可能会重复

    Zookeeper实现分布式锁原理

         使用zookeeper创建临时序列节点来实现分布式锁,大体思路就是创建临时序列节点,找出最小的序列节点,获取分布式锁,程序执行完成之后此序列节点消失,通过watch来监控节点的变化,从剩下的节点的找到最小的序列节点,获取分布式锁,执行相应处理,依次类推……

          因为zk节点唯一的,不能重复,节点类型为临时节点, 一台zk服务器创建成功时候,另外的zk服务器创建节点时候就会报错,该节点已经存在。这时候其他的zk服务器就会开始监听并等待。让这台zk服务器的程序现在执行完毕,释放锁。关闭当前会话。临时节点就会消失,并且事件通知Watcher,其他的就会来创建。

    代码实现

     创建锁的接口

    public interface ExtLock {  
    
        //ExtLock基于zk实现分布式锁
        public void  getLock();
    
        //释放锁
        public void unLock();
    
    }

    实现zk分布式锁:

    import java.util.concurrent.CountDownLatch;
    import org.I0Itec.zkclient.IZkDataListener;
    
    public class ZookeeperDistrbuteLock implements ExtLock{
    
        private static final String CONNECTION="192.168.2.222:2181";
        private ZkClient zkClient = new ZkClient(CONNECTION);
        private String lockPath="/distribute_lock";
        private CountDownLatch countDownLatch;
    
         //获取锁
          public void getLock() { 
              // 如果节点创建成果,直接执行业务逻辑,如果节点创建失败,进行等待 
              if (tryLock()) {
                System.out.println("#####成功获取锁######");
            }else {
                //进行等待
                waitLock();
            }   
        }
    
        //释放锁
          public void unLock() {
            //执行完毕 直接连接
              if (zkClient != null) {
                zkClient.close();
                System.out.println("######释放锁完毕######");
            }
        }
    
         public boolean tryLock() {
            try {
                zkClient.createEphemeral(lockPath);
                return true;
            } catch (Exception e) {
                // 如果失败 直接catch
                return false;
            }
        }
    
        public void waitLock() {
            IZkDataListener iZkDataListener = new IZkDataListener() {
                // 节点被删除
                public void handleDataDeleted(String arg0) throws Exception {
                    if (countDownLatch != null) {
                        countDownLatch.countDown(); // 计数器为0的情况,await 后面的继续执行
                    }
                }
    
         // 节点被修改
          public void handleDataChange(String arg0, Object arg1) throws Exception {
            System.out.println("########节点被修改#######");
                }
            };
    
            // 监听事件通知
            zkClient.subscribeDataChanges(lockPath, iZkDataListener);
            // 控制程序的等待
            if (zkClient.exists(lockPath)) {  //如果检查出已经被创建了就等待
                countDownLatch = new CountDownLatch(1);
                try {
                    countDownLatch.wait(); //当为0时候,后面的继续执行
                } catch (Exception e) {
                }
            }
            //后面代码继续执行
            //删除该事件监听
            zkClient.unsubscribeDataChanges(lockPath, iZkDataListener);
        }
    }

    生产订单号:

    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    //生成订单号 时间戳
    public class OrderNumGenerator {
    
      //区分不同的订单号
        private static int count = 0;
    
    //单台服务器,多个线程 同事生成订单号
        public String getNumber(){
            SimpleDateFormat simpt = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");  
            return simpt.format(new Date()) + "-" + ++count;  //时间戳后面加了 count
        }
    }

    运行方法:

    public class OrderService implements Runnable {
    
        private OrderNumGenerator orderNumGenerator = new OrderNumGenerator(); 
        private ExtLock lock = new ZookeeperDistrbuteLock();
    
        public void run() {
            getNumber();
        }
    
        public void getNumber() { 
                lock.getLock();
                String number = orderNumGenerator.getNumber();
                System.out.println(Thread.currentThread().getName() + ",number" + number);
               try {
                Thread.sleep(10000);//为了看效果
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
                 lock.unLock();
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < 100; i++) { // 开启100个线程
                //模拟分布式锁的场景
                new Thread(new OrderService()).start();
            }
        }
    }

    运行结果:

  • 相关阅读:
    连接心跳问题
    超时时间已到
    CSS定位属性-position
    AJAX背景技术介绍
    mysql中length字符长度函数使用方法
    mysql常用函数
    php构造函数的继承方法
    属性选择器(通常用在input)
    input标签常用属性
    PHP程序如何debug?
  • 原文地址:https://www.cnblogs.com/ericz2j/p/11169075.html
Copyright © 2011-2022 走看看