当前位置: 首页 > news >正文

面试官问我:Zookeeper实现分布式锁的原理是什么?

听说微信搜索《Java鱼仔》会变更强哦!

本文收录于JavaStarter ,里面有我完整的Java系列文章,学习或面试都可以看看哦

(一)引言

在单体环境中,遇到临界资源的时候我们会使用Synchronized或者RetreenLock在调用临界资源前上锁。但是在分布式的环境下,锁住单体资源就不起作用了,这个时候就需要用到分布式锁。分布式锁的原理就是借用外部的一个系统来充当锁的作用,比如Mysql、Redis、Zookeeper等都可以用作分布式锁。在实际业务中,Redis和Zookeeper用到的最多。

(二)Zookeeper锁的原理

锁分为两种:共享锁(读锁)和排他锁(写锁)
读锁:当有一个线程获取读锁后,其他线程也可以获取读锁,但是在读锁没有完全被释放之前,其他线程不能获取写锁。
写锁:当有一个线程获取写锁后,其他线程就无法获取读锁和写锁了

zookeeper有一种节点类型叫做临时序号节点,它会按序号自增地创建临时节点,这正好可以作为分布式锁的实现工具。

读锁获取原理:
1、根据资源的id创建临时序号节点:/lock/mylockR0000000005 Read
2、获取/lock下的所有子节点,判断比他小的节点是否全是读锁,如果是读锁则获取锁成功
3、如果不是,则阻塞等待,监听自己的前一个节点。
4、当前面一个节点发生变更时,重新执行第二步操作。

写锁获取原理:
1、根据资源的id创建临时序号节点:/lock/mylockW0000000006 Write
2、获取 /lock 下所有子节点,判断最小的节点是否为自己,如果是则获锁成功
3、如果不是,则阻塞等待,监听自己的前一个节点
4、当前面一个节点发生变更时,重新执行第二步。

通过一张图更清晰地看出现象:首先是写锁,因为写锁不是最前面的节点,所以阻塞了,008读锁因为前面并不是所有都是读锁,所以阻塞了

Zookeeper分布式锁原理
释放锁
删除对应的临时节点即可,如果服务器宕机了,因为临时节点的原理也不会发生死锁的情况。

(三)代码实现

真实的场景中,一般来说为了效率不会上读锁,想想看如果有人在查看数据,你就不能去修改了,这样效率是不是特别低。这里用代码实现分布式写锁,首先自己定义一个锁类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Lock {
    private String lockId;
    private String path;
    private boolean active;

    public Lock(String lockId, String nodePath) {
        this.lockId=lockId;
        this.path=nodePath;
    }
}

再通过Zookeeper写一个加锁工具类,代码已经给了注释,里面的实现原理和上面所讲的写锁获取原理一致:

public class ZookeeperLock {
    private String server="192.168.78.128:2181";
    private ZkClient zkClient;
    private static final String rootPath="/lock";


    //初始化ZkClient,并创建根节点
    public ZookeeperLock(){
        zkClient=new ZkClient(server,5000,20000);
        buildRoot();
    }

    //创建根节点
    public void buildRoot(){
        //如果根节点不存在,就创建
        if (!zkClient.exists(rootPath)){
            zkClient.createPersistent(rootPath);
            System.out.println("创建根节点成功");
        }
    }

    public Lock lock(String lockId,long timeout){
        //创建一个临时节点
        Lock lockNode=createLockNode(lockId);
        //尝试去激活锁
        lockNode=tryActiveLock(lockNode);
        //如果没有激活,则等待timeout的时间
        if (!lockNode.isActive()){
            try {
                synchronized (lockNode){
                    lockNode.wait(timeout);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //timeout时间内节点还未释放,就报lock timeout错误
        if (!lockNode.isActive()){
            throw new RuntimeException("lock timeout");
        }
        return lockNode;
    }

    //释放锁
    public void unlock(Lock lock){
        if (lock.isActive()){
            zkClient.delete(lock.getPath());
        }
    }
    
    //尝试激活锁
    private Lock tryActiveLock(Lock lockNode){
        //获取所有的子节点
        List<String> childList = zkClient.getChildren(rootPath)
                .stream()
                .sorted()
                .map(p -> rootPath + "/" + p)
                .collect(Collectors.toList());
        //获取第一个元素
        String firstNodePath = childList.get(0);
        //如果自己就是第一个节点,就激活锁
        if (firstNodePath.equals(lockNode.getPath())){
            lockNode.setActive(true);
        }else {
            //否则监听前一个锁
            String upNodePath = childList.get(childList.indexOf(lockNode.getPath())-1);
            zkClient.subscribeDataChanges(upNodePath, new IZkDataListener() {
                @Override
                public void handleDataChange(String s, Object o) throws Exception {

                }
                //如果前面一个节点被删除了,再次尝试获取锁
                @Override
                public void handleDataDeleted(String s) throws Exception {
                    System.out.println("节点删除"+s);
                    Lock lock=tryActiveLock(lockNode);
                    synchronized (lockNode){
                        if (lock.isActive()){
                            lockNode.notify();
                        }
                    }
                    zkClient.unsubscribeDataChanges(upNodePath,this);
                }
            });
        }
        return lockNode;
    }

    public Lock createLockNode(String lockId) {
        String nodePath = zkClient.createEphemeralSequential(rootPath + "/" + lockId, "lock");
        return new Lock(lockId, nodePath);
    }

}

(四)测试

上面写的这个工具类,以后可以直接拿过来用,我们来测试一下,首先是不加锁开100个线程去加一个变量:

public class Test {
    private int flag=0;
    private ZookeeperLock zookeeperLock=new ZookeeperLock();
    @Test
    public void testLock() throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 100; i++) {
            executorService.submit(()->{
                flag++;
            });
        }

        executorService.shutdown();
        executorService.awaitTermination(10, TimeUnit.SECONDS);
        System.out.println(flag);
    }
}

最后的返回结果永远到不了100,因为存在更新丢失。
加上锁:

public class Test {
    private int flag=0;
    private ZookeeperLock zookeeperLock=new ZookeeperLock();
    @Test
    public void testLock() throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 100; i++) {
            executorService.submit(()->{
                Lock lock = zookeeperLock.lock("myLock", 60 * 1000);
                flag++;
                zookeeperLock.unlock(lock);
            });
        }

        executorService.shutdown();
        executorService.awaitTermination(10, TimeUnit.SECONDS);
        System.out.println(flag);
    }
}

最后的结果一直都是100。

(五)总结

只要懂得分布式锁的原理,代码的实现就会变得十分简单。你会累是因为你在走上坡路!我们下期再见。

相关文章:

  • typedef与#define的区别
  • 一步步教你如何在SpringBoot项目中引入支付功能
  • OSChina 周三乱弹 ——你是有多寂寞啊,看光头强都……
  • 今天不聊技术,谈谈我眼中的程序员到底是个怎样的职业
  • 关于JVM调优,我理了一些工具和思路出来
  • 2016年4月20日***学习总结
  • 关于ThreadLocal的九个知识点,看完别再说不懂了
  • Java程序员需要知道的操作系统知识汇总(持续更新)
  • Tkinter之输入框操作
  • 平稳运行半年的系统宕机了,记录一次排错调优的全过程!
  • 服务发现、配置中心,Nacos帮我们都搞定了
  • 我竟从一道算法题中看到了浪漫
  • GRUB启动命令详解
  • 财务说账单上少了一分钱,老板看到代码气疯了
  • floyd算法迪杰斯特拉算法
  • 《Java编程思想》读书笔记-对象导论
  • 0基础学习移动端适配
  • Android路由框架AnnoRouter:使用Java接口来定义路由跳转
  • Angular js 常用指令ng-if、ng-class、ng-option、ng-value、ng-click是如何使用的?
  • Codepen 每日精选(2018-3-25)
  • C学习-枚举(九)
  • extract-text-webpack-plugin用法
  • Java多线程(4):使用线程池执行定时任务
  • js 实现textarea输入字数提示
  • JS基础之数据类型、对象、原型、原型链、继承
  • Python_网络编程
  • Python利用正则抓取网页内容保存到本地
  • 基于 Ueditor 的现代化编辑器 Neditor 1.5.4 发布
  • 七牛云假注销小指南
  • 在GitHub多个账号上使用不同的SSH的配置方法
  • TPG领衔财团投资轻奢珠宝品牌APM Monaco
  • 带你开发类似Pokemon Go的AR游戏
  • 如何通过报表单元格右键控制报表跳转到不同链接地址 ...
  • #鸿蒙生态创新中心#揭幕仪式在深圳湾科技生态园举行
  • (安全基本功)磁盘MBR,分区表,活动分区,引导扇区。。。详解与区别
  • (十一)c52学习之旅-动态数码管
  • (转)c++ std::pair 与 std::make
  • . ./ bash dash source 这五种执行shell脚本方式 区别
  • .NET 5.0正式发布,有什么功能特性(翻译)
  • .NET Core中Emit的使用
  • .net websocket 获取http登录的用户_如何解密浏览器的登录密码?获取浏览器内用户信息?...
  • .NET6 命令行启动及发布单个Exe文件
  • .Net8 Blazor 尝鲜
  • .NET面试题解析(11)-SQL语言基础及数据库基本原理
  • .Net组件程序设计之线程、并发管理(一)
  • ??javascript里的变量问题
  • @PreAuthorize注解
  • [Android 13]Input系列--获取触摸窗口
  • [AutoSar]状态管理(五)Dcm与BswM、EcuM的复位实现
  • [C#]winform利用seetaface6实现C#人脸检测活体检测口罩检测年龄预测性别判断眼睛状态检测
  • [CF]Codeforces Round #551 (Div. 2)
  • [CISCN2019 华北赛区 Day1 Web2]ikun
  • [C语言]——柔性数组
  • [Docker]十.Docker Swarm讲解
  • [javascript]Tab menu实现