Zk实现分布式锁

本文介绍了Zookeeper实现分布式锁的原理与方式,包括保持独占和控制时序。阐述了分布式锁产生的原因,如集群环境下的线程安全等问题。对比了分布式锁的多种实现方式,如数据库、redis等,并以生成全局订单号为例,结合代码说明Zookeeper实现分布式锁的应用。

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();
        }
    }
}

运行结果:

 

转载于:https://www.cnblogs.com/ericz2j/p/11169075.html

### Zookeeper 分布式锁实现原理 Zookeeper 是一种高效的协调服务工具,能够帮助开发者在分布式环境中管理配置、同步状态以及提供一致性保障。通过其核心特性和数据模型,Zookeeper 可以被用来实现分布式锁。 #### 1. 核心特性支持 Zookeeper 提供了两种类型的节点:**持久节点**和**临时节点**,同时还支持创建有序节点(sequential nodes)。这些特性共同构成了分布式锁的基础[^3]。 - **临时节点**:当客户端断开连接时,该节点会自动删除。这一特性确保了即使某个进程崩溃或网络中断,锁也能被安全释放。 - **有序节点**:每次创建新节点时都会附加一个递增的序列号,这使得多个竞争者可以通过比较序列号来判断谁拥有锁。 #### 2. 锁的获取流程 为了获得锁,客户端会在指定路径下创建一个带有 `EPHEMERAL_SEQUENTIAL` 属性的子节点。随后,它会读取当前路径下的所有子节点并按序排列。如果发现自己创建的节点是最小的一个,则表示成功获得了锁;否则需要监听前驱节点的变化事件[^4]。 一旦前驱节点消失(即持有锁的客户端主动释放或者因异常退出),当前等待中的客户端会被触发回调函数,并重新尝试获取锁[^1]。 #### 3. 锁的释放流程 释放锁的过程相对简单,只需调用 API 删除之前创建的那个特定子节点即可。由于这是个原子操作,在正常情况下不会引发任何竞态条件问题[^2]。 #### 4. 异常处理机制 考虑到实际运行环境可能存在各种不确定性因素,因此还需要设计合理的错误恢复策略。例如,对于会话超时导致的数据丢失情况,应用程序应该有能力检测到这种状况并对受影响的操作采取补偿措施。 ```java // Java 示例代码展示如何使用 Curator Framework 来简化 zk 客户端编程过程 import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.recipes.locks.InterProcessMutex; public class DistributedLockExample { public static void main(String[] args) throws Exception { CuratorFramework client = ...; // 初始化 curator 客户端 InterProcessMutex lock = new InterProcessMutex(client, "/lock_path"); try { lock.acquire(); // 获取锁 System.out.println("Lock acquired."); // 执行业务逻辑 } finally { lock.release(); // 确保最终总是要释放锁 System.out.println("Lock released."); } } } ``` 以上就是基于 zookeeper 实现分布式锁的主要思路及其具体步骤描述。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值