Zookeeper分布式锁

分布式锁实现原理

因为Zookerper中的节点是不可重复创建的,因此,可以起到和redis中的setnx一样的作用,在分布式环境下,充当分布式锁来控制资源的并发处理。

bin/zkCli.sh  -server 192.168.0.108:2181,192.168.0.108:2182,192.168.0.108:2183,192.168.0.108:2184

客户端1创建节点/lock

客户端2创建节点/lock

会因为已存在无法创建

获取锁流程

 

如上操作虽然可以实现分布式锁,但只适用于并发比较小的场景下;若是并发数量很大,会同时导致多个客户端线程监听加锁的节点,当锁被释放(节点被删除)的时候,要通知到所有获取失败且在监听的客户端,并再次触发竞争,这就是羊群效应而且,因为没有有序的队列作为排序,这种竞争是非公平的。

因此,为了解决该问题,可以使用临时有序节点进行分布式锁的公平控制。

其实现步骤:

1.客户端建立连接后,创建临时有序节点

2.判断自己创建的节点是否是父节点下最小序号节点,是则获取锁;否则监听上一序号节点

3.执行对应的业务逻辑,释放锁,通知下一节点获取锁,重复步骤2。

中断节点

假设线程1先获取锁执行业务逻辑,还未释放锁的时候,节点2客户端断开连接,此时也会通知节点3的客户端进行步骤2判断,此时若节点1还存在,则它最小,节点3的客户端监听节点1。

幽灵节点

客户端创建节点成功,但是服务端响应失败,导致客户端不知道之前的创建结果,会进行重连然后创新创建,这样会导致实际最先创建的子节点一直存在。

解决方式:

节点创建时会带上自己客户端的唯一标识,并将创建的子节点缓存在本地,在重连后会先判断是否存在该标识的子节点,若存在则不重复创建。

如上借助于临时顺序节点,可以避免同时多个节点的并发竞争锁,缓解了服务端压力。这种实现方式所有加锁请求都进行排队加锁,是公平锁的具体实现。

 

解决方式

使用Zookerper建立分布式锁,利用使用curtor建立

互斥锁

初始化

    @Bean(initMethod = "start")
    public CuratorFramework curatorFramework(){
        //每隔1s重试,最多重试3次
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework client = CuratorFrameworkFactory.newClient("192.168.0.108:2181", retryPolicy);
        return client;
    }

 加锁

@PostMapping("/stock/deduct")
    public Object reduceStock(Integer id) throws Exception {
       
        InterProcessMutex interProcessMutex = new InterProcessMutex(curatorFramework, "/product_" + id);

        try {
            // 加锁
            interProcessMutex.acquire();
            orderService.reduceStock(id);

        } catch (Exception e) {
            if (e instanceof RuntimeException) {
                throw e;
            }
        }finally {
            //释放锁 避免死锁
            interProcessMutex.release();
        }
        return "ok:" + port;
    }

 加锁逻辑分析

private boolean internalLock(long time, TimeUnit unit) throws Exception {
        Thread currentThread = Thread.currentThread();
        InterProcessMutex.LockData lockData = (InterProcessMutex.LockData)this.threadData.get(currentThread);
        //判断是否加过锁 
        if (lockData != null) {
            //锁重入
            lockData.lockCount.incrementAndGet();
            return true;
        } else {
            //尝试加锁
            String lockPath = this.internals.attemptLock(time, unit, this.getLockNodeBytes());
            //加锁后缓存到本地
            if (lockPath != null) {
                InterProcessMutex.LockData newLockData = new InterProcessMutex.LockData(currentThread, lockPath);
                this.threadData.put(currentThread, newLockData);
                return true;
            } else {
                return false;
            }
        }
    }

尝试加锁逻辑

String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception {
        long startMillis = System.currentTimeMillis();
        Long millisToWait = unit != null ? unit.toMillis(time) : null;
        byte[] localLockNodeBytes = this.revocable.get() != null ? new byte[0] : lockNodeBytes;
        int retryCount = 0;
        String ourPath = null;
        boolean hasTheLock = false;
        boolean isDone = false;

        while(!isDone) {
            isDone = true;

            try {
                
                ourPath = this.driver.createsTheLock(this.client, this.path, localLockNodeBytes);
                hasTheLock = this.internalLockLoop(startMillis, millisToWait, ourPath);
            } catch (NoNodeException var14) {
                if (!this.client.getZookeeperClient().getRetryPolicy().allowRetry(retryCount++, System.currentTimeMillis() - startMillis, RetryLoop.getDefaultRetrySleeper())) {
                    throw var14;
                }

                isDone = false;
            }
        }

        return hasTheLock ? ourPath : null;
    }

创建节点

容器节点的方式进行创建,其特性在于若容器节点中无字节的,会被清理掉

public String createsTheLock(CuratorFramework client, String path, byte[] lockNodeBytes) throws Exception {
        String ourPath;
        if (lockNodeBytes != null) {
            //创建容器节点creatingParentContainersIfNeeded
            //临时顺序字节点EPHEMERAL_SEQUENTIAL
            ourPath = (String)((ACLBackgroundPathAndBytesable)client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL)).forPath(path, lockNodeBytes);
        } else {
            ourPath = (String)((ACLBackgroundPathAndBytesable)client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL)).forPath(path);
        }

        return ourPath;
    }

判断是否最小

private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception {
        boolean haveTheLock = false;
        boolean doDelete = false;

        try {
            if (this.revocable.get() != null) {
                ((BackgroundPathable)this.client.getData().usingWatcher(this.revocableWatcher)).forPath(ourPath);
            }

            while(this.client.getState() == CuratorFrameworkState.STARTED && !haveTheLock) {
                //获取所有子节点并排序
                List<String> children = this.getSortedChildren();
                
                String sequenceNodeName = ourPath.substring(this.basePath.length() + 1);
                //判断是否是最小的子节点
                PredicateResults predicateResults = this.driver.getsTheLock(this.client, children, sequenceNodeName, this.maxLeases);
                if (predicateResults.getsTheLock()) {
                    haveTheLock = true;
                } else {
                    
                    String previousSequencePath = this.basePath + "/" + predicateResults.getPathToWatch();
                    synchronized(this) {
                        try {
                            
                            ((BackgroundPathable)this.client.getData().usingWatcher(this.watcher)).forPath(previousSequencePath);
                            if (millisToWait == null) {
                                this.wait();
                            } else {
                                millisToWait = millisToWait - (System.currentTimeMillis() - startMillis);
                                startMillis = System.currentTimeMillis();
                                if (millisToWait > 0L) {
                                    this.wait(millisToWait);
                                } else {
                                    doDelete = true;
                                    break;
                                }
                            }
                        } catch (NoNodeException var19) {
                        }
                    }
                }
            }
        } catch (Exception var21) {
            ThreadUtils.checkInterrupted(var21);
            doDelete = true;
            throw var21;
        } finally {
            if (doDelete) {
                this.deleteOurPath(ourPath);
            }

        }

        return haveTheLock;
    }

子节点排序

    public static List<String> getSortedChildren(CuratorFramework client, String basePath, final String lockName, final LockInternalsSorter sorter) throws Exception {
        try {
            List<String> children = (List)client.getChildren().forPath(basePath);
            List<String> sortedList = Lists.newArrayList(children);
            Collections.sort(sortedList, new Comparator<String>() {
                public int compare(String lhs, String rhs) {
                    return sorter.fixForSorting(lhs, lockName).compareTo(sorter.fixForSorting(rhs, lockName));
                }
            });
            return sortedList;
        } catch (NoNodeException var6) {
            return Collections.emptyList();
        }
    }

watch监听唤醒

当锁释放后会唤醒等待的客户端

    default CompletableFuture<Void> postSafeNotify(Object monitorHolder) {
        return this.runSafe(() -> {
            synchronized(monitorHolder) {
                monitorHolder.notifyAll();
            }
        });
    }

共享锁

不管是使用单节点的分布式锁,还是使用子节点的临时顺序节点加锁,锁的特性都是互斥的,即同一时间只能有一个请求获取到锁进行处理,若是在大并发的场景下,性能是会急剧下降的,因此,针对这类情况,可以使用共享锁来处理。

问题类型

但由于实际业务复杂性,单纯的共享锁也存在一些问题。

双写不一致

上图,线程1在写数据库后,还未更新到缓存中时;此时线程2有对同一个值有了更新,且比线程1先更新到缓存中;在线程2更新缓存后,线程1又去更新缓存,会导致预期缓存中的结果应该是线程2的更新值,实际变为线程1的更新值,与数据库中最新的值(线程2的更新值)不一致。

读写并发不一致

另一种仅仅是在首次查询后去更新缓存的处理,也存在问题。

上图,线程1、2先后更新了数据库并删除了缓存,但线程3在线程2写数据库之前读取了线程1的数据库结果,并在线程2删除缓存后更新了缓存中值;导致预期缓存值应该是8结果更新为10。

解决方式

  • 若是读请求,且前一个请求是读,则获取锁,若是写请求,则监听最后的写请求,等待锁释放;
  • 若是写请求,则判断前面是否有请求,若有,则等待其全部释放。

 共享锁的监听机制,和公平锁类似,区别在于,同是读锁,线程2和1都能获取锁,但写锁线程3需要等待1和2释放才能获取到,先监听节点2,若其释放,再遍历子节点,看线程1是否释放,若是,则获取锁,否则等待;而线程4需要监听等待节点3释放才可以获取锁;同为写请求的节点7只需要监听节点6即可,机制和公平锁一样。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值