Zk实现分布式锁

Posted ericz2j

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 synchronized 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();
        
    

运行结果:

技术图片

技术图片

 

以上是关于Zk实现分布式锁的主要内容,如果未能解决你的问题,请参考以下文章

Zk实现分布式锁

zookeeper实现分布式锁

ZK实现分布式锁

ZK实现分布式锁

最简单分布式锁ZK实现方案

ZK(ZooKeeper)分布式锁实现