zookeeper - 实现分布式锁

zookeeper实现分布式锁

一:锁的特点与原生zookeeper

1:普通锁有什么特点

在这里插入图片描述

2:为什么zookeeper可以用来实现锁

  1. 同一个父目录下面不能有相同的子节点,这就是zookeeper的排他性
  2. 通过JDK的栅栏来实现阻塞性
  3. 可重入性我们可以通过计数器来实现【count += 1】

在这里插入图片描述

3:创建客户端的核心类:Zookeeper

org.apache.zookeeper
org.apache.zookeeper.data

下面是核心方法:

方法作用
connect连接到zk集合
create创建znode
exist检查znode是否存在及其信息
getData从特定的znode获取数据
setData从特定的znode设置数据
getChildren获取特定znode中的所有子节点
delete删除特定znode及其所有子项
close关闭连接

二:使用第三方客户端zkClient来简化操作

<dependency>
    <groupId>com.101tec</groupId>
    <artifactId>zkclient</artifactId>
    <version>0.10</version>
</dependency>

1:实现序列化接口

package my_zk;

import org.I0Itec.zkclient.exception.ZkMarshallingError;
import org.I0Itec.zkclient.serialize.ZkSerializer;

import java.nio.charset.StandardCharsets;

/**
 * <p>
 * 功能描述:实现ZK序列化接口
 * </p>
 *
 * @author cui haida
 * @date 2024/03/04/11:19
 */
public class MyZkSerializer implements ZkSerializer {

    /**
     * 正常来说我们还需要进行一个非空判断,这里为了省事没做,不过严格来说是需要做的
     * 就是简单的转换
     */
    @Override
    public byte[] serialize(Object data) throws ZkMarshallingError {
        String d = (String) data;
        return d.getBytes(StandardCharsets.UTF_8);
    }

    @Override
    public Object deserialize(byte[] bytes) throws ZkMarshallingError {
        return new String(bytes, StandardCharsets.UTF_8);
    }
}

2:zkclient的简单使用

package my_zk;

import lombok.extern.slf4j.Slf4j;
import org.I0Itec.zkclient.IZkChildListener;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.apache.zookeeper.CreateMode;

import java.util.List;

/**
 * <p>
 * 功能描述:Zk客户端实现
 * </p>
 *
 * @author cui haida
 * @date 2024/03/04/11:23
 */
@Slf4j
public class ZkClientDemo {
    public static void main(String[] args) {
        // 创建一个zk客户端
        ZkClient client = new ZkClient("localhost:2181");
        // 实现序列化接口
        client.setZkSerializer(new MyZkSerializer());
        // 创建一个节点zk,在zk节点下再创建一个子节点app6,赋值123
        // 在之前也已经提到了,zookeeper中的节点既是文件夹也是文件
        // 源码中CreateMode是一个枚举,PERSISTENT---当客户端断开连接时,znode不会自动删除,也就是声明这个节点是一个持久节点
        client.create("/zk/app6", "123", CreateMode.PERSISTENT);

        client.subscribeChildChanges("/zk/app6", new IZkChildListener() {
            @Override
            public void handleChildChange(String parentPath, List<String> currentChildren) throws Exception {
                System.out.println(parentPath + "子节点发生变化:" + currentChildren);
            }
        });

        // 这里开始创建一个watch,但是为什么这个方法会命名为subscribeDataChanges,有如下的原因:
        // 1:原本watch的设置然后获取是仅一次性的,现在我们使用subscribe这个英文,代表订阅,代表这个watch一直存在
        // 2:使用这个方法我们可以轻易实现持续监听的效果,比原生zookeeper方便
        client.subscribeDataChanges("zk/app6", new IZkDataListener() {
            @Override
            public void handleDataChange(String dataPath, Object data) {
                System.out.println(dataPath + "节点的值发生变化:" + data);
            }

            @Override
            public void handleDataDeleted(String dataPath) {
                System.out.println(dataPath + "节点被删除");
            }
        });
        
        try {
            Thread.currentThread().join();
        } catch (InterruptedException e) {
            log.error("线程被异常中断");
            e.printStackTrace();
        }
    }
}

测试监听事件

  • create /zk/app6/tellYourDream时—控制台打印/zk/app6子节点发生变化:[tellYourDream]
  • delete /zk/app6/tellYourDream—控制台打印/zk/app6子节点发生变化:[],此时已经不存在任何节点,所以为空
  • set /zk/app6 123456—/zk/app6发生变化:123456
  • delete /zk/app6—同时触发了两个监听事件,/zk/app6子节点发生变化:null 和 /zk/app6节点被删除

在这里插入图片描述

3:CreateMode的补充

  • 持久化节点:不删除节点永远存在。且可以创建子节点
  • 非持久节点,换言之就是临时节点,临时节点就是客户端连接的时候创建,客户端挂起的时候,临时节点自动删除。不能创建子节点

在这里插入图片描述

三:Zookeeper实现分布式锁

1:节点不可重名+watch机制

之前有提到,zookeeper中同一个子节点下面的节点名称是不能相同的,我们可以利用这个互斥性,就可以实现分布式锁的工具
临时节点就是创建的时候存在,消失的时候,节点自动删除,当客户端失联,网络不稳定或者崩溃的时候,这个通过临时节点所创建的锁就会自行消除。这样就可以完美避免死锁的问题。所以我们利用这个特性,实现我们的需求。
原理其实就是节点不可重名+watch机制。
比如说我们的程序有多个服务实例,哪个服务实例都去创建一个lock节点,谁创建了,谁就获得了锁,剩下我们没有创建的应用,就去监听这个lock节点,如果这个lock节点被删除掉,这时可能出现两种情况,一就是客户端连不上了,另一种就是客户端释放锁,将lock节点给删除掉了。

在这里插入图片描述

package my_zk;

import cn.hutool.core.util.StrUtil;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.I0Itec.zkclient.exception.ZkNodeExistsException;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * <p>
 * 功能描述:
 * </p>
 *
 * @author cui haida
 * @date 2024/03/04/14:04
 */
public class ZkDistributeLock implements Lock {

    // 我们需要一个锁的目录
    private String lockPath;

    // 我们需要一个客户端
    private ZkClient client;

    // 构造函数传值
    public ZkDistributeLock(String lockPath) {
        if (StrUtil.isBlank(lockPath)) {
            throw new IllegalStateException("path不能为空白字符串");
        }
        this.lockPath = lockPath;

        client = new ZkClient("localhost:2181");
        client.setZkSerializer(new MyZkSerializer());
    }


    @Override
    public void lock() {
        if (!tryLock()) {
            waitForLock(); // 如果没有获得锁,阻塞自己
            lock(); // 从阻塞中唤醒,再次尝试获得锁
        }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        try {
            client.createEphemeral(lockPath); // 创建临时节点
        } catch (ZkNodeExistsException e) {
            return false;
        }
        return true;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public void unlock() {
        client.delete(lockPath);
    }

    @Override
    public Condition newCondition() {
        return null;
    }

    private void waitForLock() {
        final CountDownLatch cdl = new CountDownLatch(1);
        IZkDataListener listener = new IZkDataListener() {
            @Override
            public void handleDataDeleted(String dataPath) throws Exception {
                System.out.println("----收到节点被删除了-------------");
                //唤醒阻塞线程
                cdl.countDown();
            }

            @Override
            public void handleDataChange(String dataPath, Object data) throws Exception {
            }
        };

        // 订阅数据变化的监听,监听的父节点路径是lockPath
        client.subscribeDataChanges(lockPath, listener);
        
        // 阻塞自己
        if (this.client.exists(lockPath)) {
            try {
                cdl.await(); // 直到监听的前一个节点被唤醒,调用了countDown()才会跳出阻塞
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 取消注册
        client.unsubscribeDataChanges(lockPath, listener);
    }
}
获取锁,创建节点后

    1.成功获取到的---执行业务---然后释放锁
                                    |
                                    |
                                    |               
    2.获取失败,注册节点的watch---阻塞等待---取消watch---再回到获取锁,创建节点的判断

惊群效应

比如我的实例现在有无数个,此时我们的lock每次被创建,有人获取了锁之后,其他的人都要被通知阻塞,此时我们就浪费了很多的网络资源,也就是惊群效应。

2:取号 + 最小号取lock + watch

在这里插入图片描述

在这里插入图片描述

package com.atguigu.case2;

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;

/**
 * 分布式锁
 * @author cuihaida
 */
public class DistributedLock {

    private ZooKeeper zooKeeper;

    private CountDownLatch countDownLatch = new CountDownLatch(1);
    private CountDownLatch waitLatch = new CountDownLatch(1);
    private String waitPath;
    private String curNode;

    public DistributedLock() throws IOException, InterruptedException, KeeperException {
        // 获取连接
        // 注意:逗号的左右两端不能又空格
        String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181";
        int sessionTimeout = 2000;
        
        // 连接
        zooKeeper = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent watchedEvent) {
                // 如果连接上zk了,可以释放
                if (watchedEvent.getState() == Event.KeeperState.SyncConnected) {
                    countDownLatch.countDown();
                }
                // 存在节点删除,并且删除的是当前节点监听的前一个节点时候,countDown
                if (watchedEvent.getType() == Event.EventType.NodeDeleted && watchedEvent.getPath().equals(waitPath)) {
                    waitLatch.countDown();
                }
            }
        });

        // 成功连接后,才往下走
        countDownLatch.await();

        // 判断根节点的/locks是不是存在
        Stat exists = zooKeeper.exists("/locks", false);
        if (exists == null) {
            // 创建根节点
            String locks = "locks";
            zooKeeper.create("/locks", locks.getBytes(StandardCharsets.UTF_8), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT
            );
        }

    }

    /**
     * 核心逻辑,zk分布式锁
     * 1:创建临时的,带有序号的节点
     * 2:判断当前创建的节点是不是最小的序号节点
     * 		如果是,就获取到锁
     * 		如果不是,就监听他序号的前一个
     */
    public void zkLock() throws KeeperException, InterruptedException {
        // 创建临时带序号的节点
        curNode = zooKeeper.create("/locks/seq-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

        // 判断当前创建的节点是不是最小的序号节点,如果是就获取到锁,如果不是,就监听他序号的前一个
        List<String> children = zooKeeper.getChildren("/locks", false);
        if (children.size() == 1) {
            return ;
        } else {
            Collections.sort(children); // 通过需要排序
            String thisNode = curNode.substring("/locks".length());
            int index = children.indexOf(thisNode);
            if (index == -1) {
                System.out.println("数据异常");
            } else if (index == 0) {
                return ;
            } else {
                // 监听他的前一个节点
                waitPath = "/locks/" + children.get(index - 1);
                zooKeeper.getData(waitPath, true, null);
            }
        }
        // 只有前一个节点删除的时候往下走
        waitLatch.await();
    }

    /**
     * 解锁就是从删除当前加锁
     */
    public void zkUnlock() throws KeeperException, InterruptedException {
        // 删除正在加锁的节点节点
        zooKeeper.delete(curNode, -1);
    }
}

3:更为简单的第三方客户端—Curator

image.png

<!-- curator  -->
<dependency>
  <groupId>org.apache.curator</groupId>
  <artifactId>curator-framework</artifactId>
  <version>5.3.0</version>
</dependency>

<dependency>
  <groupId>org.apache.curator</groupId>
  <artifactId>curator-recipes</artifactId>
  <version>5.3.0</version>
</dependency>

<dependency>
  <groupId>org.apache.curator</groupId>
  <artifactId>curator-client</artifactId>
  <version>5.3.0</version>
</dependency>
package com.atguigu.case3;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;

/**
 * curator框架测试
 * @author cuihaida
 */
public class CuratorLockTest {
    public static void main(String[] args) {
        // 创建分布式锁1
        // 第一个参数是客户端,第二个参数是路径
        InterProcessMutex interProcessMutex01 = new InterProcessMutex(getCuratorFramework(), "/locks");
        // 创建分布式锁2
        InterProcessMutex interProcessMutex02 = new InterProcessMutex(getCuratorFramework(), "/locks");

        
        // -------------- 下面是测试 -----------------
        new Thread(() -> {
            try {
                interProcessMutex01.acquire();
                System.out.println("线程1启动, lock1获取到锁");
                interProcessMutex01.acquire();
                System.out.println("线程1启动, lock1再次获取到锁");
                Thread.sleep(5000);
                interProcessMutex01.release();
                System.out.println("lock1释放锁");
                interProcessMutex01.release();
                System.out.println("lock1再次释放锁");
            } catch (Exception e) {
                System.out.println("error");
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            try {
                interProcessMutex02.acquire();
                System.out.println("线程2启动, lock2获取到锁");
                interProcessMutex02.acquire();
                System.out.println("线程2启动, lock2再次获取到锁");
                Thread.sleep(5000);
                interProcessMutex02.release();
                System.out.println("lock2释放锁");
                interProcessMutex02.release();
                System.out.println("lock2再次释放锁");
            } catch (Exception e) {
                System.out.println("error");
                e.printStackTrace();
            }
        }).start();
    }
    
    

    /**
     * 客户端连接
     * @return 客户端
     */
    private static CuratorFramework getCuratorFramework() {
        String connectString = "hadoop102:2181,hadoop103:2181,hadoop104:2181";
        int sessionTimeout = 2000;
        ExponentialBackoffRetry exponentialBackoffRetry = new ExponentialBackoffRetry(3000, 3);
        CuratorFramework client = CuratorFrameworkFactory.builder()
                // 连接地址
                .connectString(connectString)
                // 超时时间
                .sessionTimeoutMs(sessionTimeout)
                // 重试设置
                .retryPolicy(exponentialBackoffRetry)
                .build();
        client.start();
        System.out.println("zookeeper启动成功");
        return client;
    }
}

四:zk分布式锁和redis分布式锁的对比

1:Redis 实现的分布式锁

Redis 实现的分布式锁的话,不能够 100% 保证可用性 ,因为在真实环境中使用分布式锁,一般都会集群部署 Redis ,来避免单点问题

那么 Redisson 去 Redis 集群上锁的话,先将锁信息写入到主节点中,如果锁信息还没来得及同步到从节点中,主节点就宕机了,就会导致这个锁信息丢失

并且在分布式环境下可能各个机器的时间不同步,都会导致加锁时出现一系列无法预知的问题

因此 RedLock 被 Redis 作者提出用于保证在集群模式下加锁的可靠性,就是去多个 Redis 节点上都尝试加锁

超过一半节点加锁成功,并且加锁后的时间要保证没有超过锁的过期时间,才算加锁成功

具体的流程比较复杂,并且性能较差,了解一下即可

所以说呢,在分布式环境下,使用 Redis 做分布式锁的话,或多或少都可能会产生一些未知的问题

并且 Redis 本质上来说也不是做分布式协调的,他只是作为一个分布式缓存解决方案存在

如果业务不追求非常高的可靠性,并且需要使用到公平锁、可重入锁、读写锁这类功能的话,可以选用 Redisson 分布式锁

2:Zookeeper分布式锁

ZooKeeper 的分布式锁的特点就是:稳定、健壮、可用性强

这得益于 ZooKeeper 这个框架本身的定位就是用来做 分布式协调 的,因此在需要保证可靠性的场景下使用 ZooKeeper 做分布式锁是比较好的

ZooKeeper 的分布式锁是 基于临时节点 来做的,多个客户端去创建临时同一个节点,第一个创建客户端抢锁成功,释放锁时只需要删除临时节点即可

但是缺点就是 ZooKeeper 分布式锁提供的功能比较少,没有读写锁、公平锁等等这些复杂的功能

因此 ZooKeeper 的分布式锁适用于 对可靠性要求很高 的业务场景,并且 需要的锁功能也比较简单

并且 ZooKeeper 的分布式锁在极端情况下也会存在不安全的问题:如果加锁的客户端长时间 GC 导致无法与 ZooKeeper 维持心跳,那么 ZK 就会认为这个客户端已经挂了,于是将该客户端创建的临时节点删除,那么当这个客户端 GC 完成之后还以为自己持有锁,但是它的锁其实已经没有了,因此也会存在不安全的问题

3:项目建议

具体选用哪一种分布式锁的话,可以根据需要使用的功能和已经引入的技术栈来进行选择

比如只用到简单的分布式锁功能,又恰好已经引入了 ZK 依赖,就可以使用 ZK 的分布式锁

其实这两种锁在真正的项目中使用的都是比较多的

而且要注意的是无论是使用 Redis 分布式锁还是 ZK 分布式锁其实在极端情况下都会出现问题,都不可以保证 100% 的安全性

可以通过分布式锁在上层互斥掉大量的请求,如果真有个别请求出现锁失效,可以在底层资源层做一些互斥保护,作为一个兜底

因此如果是对可靠性要求非常高的应用,不可以把线程安全的问题全部寄托于分布式锁,而是要在资源层也做一些保护,来保证数据真正的安全

而 Redis 中的 RedLock 尽量不使用,因为它为了保证加锁的安全牺牲掉了很多的性能,并且部署成本高(至少部署 5 个 Redis 的主库)

使用 Redis 分布式锁建议通过【主从 + 哨兵】部署集群,使用它的分布式锁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值