分布式锁的最佳实践之Zookeeper

本文深入探讨了Zookeeper分布式锁的实现原理与最佳实践。详细分析了锁的获取与释放流程,介绍了如何通过Zookeeper解决分布式环境下的锁问题,包括防死锁、可重入锁等特性,并提供了具体代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

从库存超卖问题分析锁和分布式锁的应用(一)
从库存超卖问题分析锁和分布式锁的应用(二)
分布式锁的最佳实践之Redisson
分布式锁的最佳实践之Zookeeper
分布式锁的最佳实践之MySQL

【1】思路分析

分布式锁的步骤:

  1. 获取锁:create一个节点
  2. 删除锁:delete一个节点
  3. 重试:没有获取到锁的请求重试

参照redis分布式锁的特点:

  1. 互斥 排他
  2. 防死锁:
    1. 可自动释放锁(临时节点) :获得锁之后客户端所在机器宕机了,客户端没有主动删除子节点;如果创建的是永久的节点,那么这个锁永远不会释放,导致死锁;由于创建的是临时节点,客户端宕机后,过了一定时间zookeeper没有收到客户端的心跳包判断会话失效,将临时节点删除从而释放锁。
    2. 可重入锁:借助于ThreadLocal
  3. 防误删:宕机自动释放临时节点,不需要设置过期时间,也就不存在误删问题。
  4. 加锁/解锁要具备原子性
  5. 单点问题:使用Zookeeper可以有效的解决单点问题,ZK一般是集群部署的。
  6. 集群问题:zookeeper集群是强一致性的,只要集群中有半数以上的机器存活,就可以对外提供服务。

实现思路:

  1. 多个请求同时添加一个相同的临时节点,只有一个可以添加成功。添加成功的获取到锁
  2. 执行业务逻辑
  3. 完成业务流程后,删除节点释放锁。

【2】简单实现

由于zookeeper获取链接是一个耗时过程,这里可以在项目启动时,初始化链接,并且只初始化一次。借助于spring特性,代码实现如下:

@Component
public class ZkClient {

    private static final String connectString = "127.0.0.1:2181";

    private ZooKeeper zooKeeper;

    @PostConstruct
    public void init(){
        try {
            zooKeeper = new ZooKeeper(connectString, 30000, new Watcher() {
                @Override
                public void process(WatchedEvent event) {
                    if (Event.KeeperState.SyncConnected.equals(event.getState())
                            && Event.EventType.None.equals(event.getType())) {
                        System.out.println("获取链接成功。。。。。。" + event);
                    }
                }
            });

        } catch (Exception e) {
            System.out.println("获取链接失败!");
            e.printStackTrace();
        }
    }

    @PreDestroy
    public void destroy(){
        try {
            if (zooKeeper != null){
                zooKeeper.close();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 初始化zk分布式锁对象方法
     * @param lockName
     * @return
     */
    public ZkDistributedLock getLock(String lockName){
        return new ZkDistributedLock(zooKeeper, lockName);
    }
}

分布式锁代码如下:

public class ZkDistributedLock {

    private static final String ROOT_PATH = "/distributed";

    private String path;

    private ZooKeeper zooKeeper;

    public ZkDistributedLock(ZooKeeper zooKeeper, String lockName){
        this.zooKeeper = zooKeeper;
        this.path = ROOT_PATH + "/" + lockName;
        // 创建分布式锁根节点
        try {
            if (this.zooKeeper.exists(ROOT_PATH, false) == null){
                this.zooKeeper.create(ROOT_PATH, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }
        } catch (KeeperException | InterruptedException e) {
            e.printStackTrace();
        }
    }

// 注意,这里使用递归方法实现自旋获取锁机制。自旋竞争资源会降低吞吐量
    public void lock(){
        try {
            // 临时节点,防止服务器宕机带来的死锁问题
            zooKeeper.create(path, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
        } catch (Exception e) {
            // 重试
            try {
                Thread.sleep(200);
                lock();
            } catch (InterruptedException ex) {
                ex.printStackTrace();
            }
        }
    }

    public void unlock(){
        try {
            this.zooKeeper.delete(path, -1);
        } catch (InterruptedException | KeeperException e) {
            e.printStackTrace();
        }
    }
}

基本实现中由于无限自旋影响性能。试想:每个请求要想正常的执行完成,最终都是要创建节点,如果能够避免争抢必然可以提高性能。

【3】阻塞锁/公平锁

分析思路:这里创建临时序列化节点

  • 所有请求要求获取锁时,给每一个请求创建临时序列化节点
  • 获取当前节点的前置节点,如果前置节点为空则获取锁成功,否则监听前置节点
  • 获取锁成功之后执行业务操作,然后释放当前节点的锁。
public class ZkDistributedLock {

    private static final String ROOT_PATH = "/distributed";

    private String lockName;

    private String currNodePath;

    private ZooKeeper zooKeeper;

    public ZkDistributedLock(ZooKeeper zooKeeper, String lockName){
        this.zooKeeper = zooKeeper;
        this.lockName =  lockName;
        // 创建分布式锁根节点
        try {
            if (this.zooKeeper.exists(ROOT_PATH, false) == null){
                this.zooKeeper.create(ROOT_PATH, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }
        } catch (KeeperException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    public boolean lock(){
        try {
            // 临时序列化节点,防止服务器宕机带来的死锁问题。这里加 - 是为了方便截取序列号
            currNodePath = zooKeeper.create(ROOT_PATH + "/" + lockName, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

            //获取前置节点,如果前置节点为空则获取锁成功,否则监听前置节点。
            String preNode = getPreNode();

            if (StringUtils.isEmpty(preNode)){
                return true;
            }
            //利用闭锁实现阻塞功能
            CountDownLatch countDownLatch=new CountDownLatch(1);
            //因为获取前置节点这个操作不具备原子性,再次判断前置节点是否存在
           if(this.zooKeeper.exists(ROOT_PATH + "/" + preNode, new Watcher() {
               @Override
               public void process(WatchedEvent watchedEvent) {
                   countDownLatch.countDown();
               }
           })==null){
               return true;
           }
           countDownLatch.await();
           return true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    public void unlock(){
        try {
            this.zooKeeper.delete(currNodePath, -1);
        } catch (InterruptedException | KeeperException e) {
            e.printStackTrace();
        }
    }
    /**
     * 获取指定节点的前节点
     * @return
     */
    private String getPreNode(){
        try {
            // 获取根路径下的所有序列化子节点
            List<String> children = this.zooKeeper.getChildren(ROOT_PATH, false);
            // 判空
            if (CollectionUtils.isEmpty(children)){
                throw new IllegalMonitorStateException("获取前置节点失败!");
            }
            // 获取和当前节点统一资源的锁
            List<String> nodes = children.stream().filter(node -> StringUtils.startsWith(node, lockName)).collect(Collectors.toList());
            // 判空
            if (CollectionUtils.isEmpty(nodes)){
                throw new IllegalMonitorStateException("获取前置节点失败!");
            }
            //排队
            Collections.sort(nodes);

            //获取当前节点下标
            String currNode = StringUtils.substringAfterLast(currNodePath, "/");//当前节点全路径对应的节点名称
            int index = Collections.binarySearch(nodes, currNode);

            if(index<0){
                throw new IllegalMonitorStateException("获取前置节点失败!");
            }else if (index >0){
                return nodes.get(index-1);//返回前置节点
            }

            return null;
        } catch (KeeperException | InterruptedException e) {
            e.printStackTrace();
            throw new IllegalMonitorStateException("获取前置节点失败!");
        }
    }
}

目前还未实现可重入功能,如果一个线程任务中连续两次获取同一把锁将会死锁。可以采用THREAD_LOCAL记录重入次数,实现重入机制。

【4】可重入锁

思路如下:

  • 使用HREAD_LOCAL记录重入次数
  • 加锁时重入次数+1
  • 解锁时重入次数-1
  • 如果解锁后重入次数为0,则删除节点
ublic class ZkDistributedLock {

    private static final String ROOT_PATH = "/distributed";

    private String lockName;

    private String currNodePath;

    private ZooKeeper zooKeeper;

    private static final ThreadLocal<Integer> THREAD_LOCAL = new ThreadLocal<>();

    public ZkDistributedLock(ZooKeeper zooKeeper, String lockName){
        this.zooKeeper = zooKeeper;
        this.lockName =  lockName;
        // 创建分布式锁根节点
        try {
            if (this.zooKeeper.exists(ROOT_PATH, false) == null){
                this.zooKeeper.create(ROOT_PATH, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }
        } catch (KeeperException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    public boolean lock(){
        try {
            // 判断是否拥有锁,如果是,则flag+1
            Integer flag = THREAD_LOCAL.get();
            if(flag!=null&&flag>0){
                THREAD_LOCAL.set(++flag);//重入
                return true;
            }
            // 临时序列化节点,防止服务器宕机带来的死锁问题
            currNodePath = zooKeeper.create(ROOT_PATH + "/" + lockName, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

            //获取前置节点,如果前置节点为空则获取锁成功,否则监听前置节点。
            String preNode = getPreNode();

            if (StringUtils.isEmpty(preNode)){
                THREAD_LOCAL.set(1);
                return true;
            }
            //利用闭锁实现阻塞功能
            CountDownLatch countDownLatch=new CountDownLatch(1);
            //因为获取前置节点这个操作不具备原子性,再次判断前置节点是否存在
           if(this.zooKeeper.exists(ROOT_PATH + "/" + preNode, new Watcher() {
               @Override
               public void process(WatchedEvent watchedEvent) {
                   countDownLatch.countDown();
               }
           })==null){
               THREAD_LOCAL.set(1);
               return true;
           }
           countDownLatch.await();
            THREAD_LOCAL.set(1);
           return true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    public void unlock(){
        try {
            THREAD_LOCAL.set(THREAD_LOCAL.get()-1);
            if(THREAD_LOCAL.get() ==0){
                this.zooKeeper.delete(currNodePath, -1);
            }
        } catch (InterruptedException | KeeperException e) {
            e.printStackTrace();
        }
    }
    /**
     * 获取指定节点的前节点
     * @return
     */
    private String getPreNode(){
        try {
            // 获取根路径下的所有序列化子节点
            List<String> children = this.zooKeeper.getChildren(ROOT_PATH, false);
            // 判空
            if (CollectionUtils.isEmpty(children)){
                throw new IllegalMonitorStateException("获取前置节点失败!");
            }
            // 获取和当前节点统一资源的锁
            List<String> nodes = children.stream().filter(node -> StringUtils.startsWith(node, lockName)).collect(Collectors.toList());
            // 判空
            if (CollectionUtils.isEmpty(nodes)){
                throw new IllegalMonitorStateException("获取前置节点失败!");
            }
            //排队
            Collections.sort(nodes);

            //获取当前节点下标
            String currNode = StringUtils.substringAfterLast(currNodePath, "/");//当前节点全路径对应的节点名称
            int index = Collections.binarySearch(nodes, currNode);

            if(index<0){
                throw new IllegalMonitorStateException("获取前置节点失败!");
            }else if (index >0){
                return nodes.get(index-1);//返回前置节点
            }

            return null;
        } catch (KeeperException | InterruptedException e) {
            e.printStackTrace();
            throw new IllegalMonitorStateException("获取前置节点失败!");
        }
    }
}

【5】Zookeeper分布式锁特点

  • 独占排他互斥:节点不重复
  • 防死锁:客户端程序获取到锁之后服务器立马宕机。临时节点:一旦客户端服务器宕机,链接就会关闭,此时zk心跳检测不到客户端程序,删除对应的临时节点
  • 防误删:给每一个请求线程创建一个唯一的序列化节点
  • 原子性:创建节点 删除节点 查询及监听 具备原子性
  • 可重入:ThreadLocal实现
  • 自动续期:没有过期时间 也就不需要自动续期
  • 单点故障:zk一般都是集群部署,zk集群偏向于一致性集群
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流烟默

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值