zookeeper实现分布式锁,吊打面试官

前言

我们之前讲了很多锁的只用,比如synchonrized、ReentrantLock、AQS等等。

这些都是单体应用下锁的一些使用。众所周知,我们现在的应用都是分布式的。所以上面那些锁已经无法满足我们了。

我们需要用到分布式锁,分布式锁一般有三种实现方式,如:MySQL数据库、Redis、zookeeper。

其中zookeeper和Redis的实现方式普通被各大公司使用,他们各自也都有优缺点。我们今天主要讲一下zookeeper分布式锁的实现。Redis分布式锁我们之后也会讲到。

zookeeper简介

Zookeeper是一个开源的分布式协调服务,是一个典型的分布式数据一致性解决方案。

分布式应用程序可以基于Zookeeper实现诸如数据发布/订阅负载均衡命名服务分布式协调/通知集群管理Master选举分布式锁分布式队列等功能。

ZK实现分布式锁的两个重要概念

ZNode节点

ZK的存储结构类似于Windows的文件系统。区别在于window他的目录是不能存储数据的,只有文件才能存储数据。

但是ZK的所有层级目录,都成为ZNode,他们都是一样的,没有目录和文件这种说法。只有父节点和子节点的概念。

无论是哪个层级的ZNode节点,都是可以存储数据的。

下面是zookeeper客户端连接工具截图:

ZNode节点种类
  • 临时节点

    客户端与zookeeper断开连接后,该节点会自动删除

  • 临时有序节点

    客户端与zookeeper断开连接后,该节点会自动删除,但是这些节点都是有序排列的。

  • 持久节点

    客户端与zookeeper断开连接后,该节点依然存在

  • 持久节点

    客户端与zookeeper断开连接后,该节点依然存在,但是这些节点都是有序排列的。

watch监听机制

主要是监听以下节点变化信息。

错误的实现分布式锁方式

锁原理

多个客户端同时去创建同一个临时节点,哪个客户端第一个创建成功,就成功的获取锁,其他客户端获取失败。

就像双十一,十万人同时秒杀一个商品,谁手速快谁就能秒杀到。

获取锁的流程

这里我们使用的是临时节点

  • 四个客户端同时创建一个临时节点。

  • 谁第一个创建成功临时节点,就代表持有了这个锁(这里临时节点就代表锁)。

  • 其他红色的客户端判断已经有人创建成功了,就开始监听这个临时节点的变化。

释放锁的流程

  • 红色线的客户端执行任务完毕,与zookeeper断开了连接。

  • 这时候临时节点会自动被删除掉,因为他是临时的。

  • 其他绿色线的客户端watch监听到临时节点删除了,就会一拥而上去创建临时节点(也就是创建锁)

存在的问题分析

当临时节点被删除的时候,其余3个客户端一拥而上抢着创建节点。3个节点比较少,性能上看不出什么问题。

那如果是一千个客户端在监听节点呢?一旦节点被删除了,会唤醒一千个客户端,一千个客户端同时来创建节点。

但是只有一个客户端能创建成功,却要让一千个客户端来竞争。

对zookeeper的压力会很大,同时浪费这些客户端的线程资源,其中有999个客户端是白跑一趟的。

这就叫做惊群现象,也叫羊群现象。

一个节点释放删除了,却要惊动一千个客户端,这种做法太傻了吧。

正确的实现分布式锁方式

这里用的是顺序临时节点。

锁原理

多个客户端来竞争锁,各自创建自己的节点,按照顺序创建,谁排在第一个,谁就成功的获取了锁。

就像排队买东西一样,谁排在第一个,谁就先买。

创建锁的过程

  • A、B、C、D四个客户端来抢锁

  • A先来了,他创建了000001的临时顺序节点,他发现自己是最小的节点,那么就成功的获取到了锁

  • 然后B来获取锁,他按照顺序创建了000001的临时顺序节点,发现前面有一个比他小的节点,那么就获取锁失败。他开始监听A客户端,看他什么时候能释放锁

  • 同理C和D。

释放锁的过程

  • A客户端执行完任务后,断开了和zookeeper的会话,这时候临时顺序节点自动删除了,也就释放了锁

  • B客户端一直在虎视眈眈的watch监听着A,发现他释放了锁,立马就判断自己是不是最小的节点,如果是就获取锁成功

  • C监听着B,D监听着C。

合理性分析

A释放锁会唤醒B,B获取到锁,对C和D是没有影响的,因为B的节点并没有发生变化。

同时B释放锁,唤醒C,C获取锁,对D是没有影响的,因为C的节点没有变化。

同理D。。。。

释放锁的操作,只会唤醒下一个客户端,不会唤醒所有的客户端。所以这种方案不存在惊群现象。

ps:创建临时节点 = 创建锁,删除临时节点 = 释放锁。

代码实现

我们这里直接用封装好的工具类,因为如果你自己写的话,如果测试不到位,一旦线上出现问题,那就是大问题。

这里我们用curator这个工具类,他这里把分布式锁已经都给我们实现好了,我们使用起来就像ReentrantLock这些锁一样,非常简单。

如果大家有兴趣,可以去阅读一下curator的源码。

pom文件配置

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>4.0.0</version>
    <exclusions>
        <exclusion>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.4.10</version>
</dependency>

java代码

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.RetryNTimes;
import java.util.concurrent.TimeUnit;

/**
 * curator分布式锁测试
 */
public class CuratorDistrLockTest implements Runnable {

  //zookeeper的地址
  private static final String ZK_ADDRESS = "127.0.0.1:2181";

  private static final String ZK_LOCK_PATH = "/zkLock";

  static CuratorFramework client = null;

  static {
      // 连接ZK,如果连接失败,设置每5000毫秒重试一次,最多重试10次
      client = CuratorFrameworkFactory.newClient(ZK_ADDRESS,
              new RetryNTimes(10, 5000));
      client.start();
  }

  private static void curatorLockTest() {

      InterProcessMutex lock = new InterProcessMutex(client, ZK_LOCK_PATH);
      try {
          if (lock.acquire(6 * 1000, TimeUnit.SECONDS)) {
              System.out.println("====== " + Thread.currentThread().getName() + " 抢到了锁 ======");
              //执行业务逻辑
              Thread.sleep(15000);
              System.out.println(Thread.currentThread().getName() + "任务执行完毕");
          }
      } catch (Exception e) {
          System.out.println("业务异常");
      } finally {
          try {
              lock.release();
          } catch (Exception e) {
              System.out.println("锁释放异常");
          }
      }
  }

  public static void main(String[] args) {
      // 用两个线程,模拟两个客户端
      // 每个线程创建各自的zookeeper连接对象
      new Thread(new CuratorDistrLockTest()).start();

      new Thread(new CuratorDistrLockTest()).start();
  }

  @Override
  public void run() {
      curatorLockTest();
  }
}

执行结果

同时观察zookeeper客户端的变化,zkLock目录下出现了两个临时顺序节点

程序结束后,我们在刷新zookeeper客户端,发现zkLock目录下的临时顺序节点已经被自动删除了。

总结

为什么不采用持久节点呢,因为持久节点必须要客户端手动删除,否则他会一直存在zookeeper中。

如果我们的客户端获取到了锁,还没释放锁就突然宕机了,那么这个锁会一直存在不被释放。导致其他客户端无法获取锁。

zookeeper实现的锁功能是比较健全的,但是性能上稍微差一些。比如zookeeper要维护集群自身信息的一致性,频繁创建和删除节点等原因。

如果仅仅是为了实现分布式锁而维护一套zookeeper集群,有点浪费了。

如果公司本来就有zookeeper集群,同时并发不是非常大的情况下,可以考虑zookeeper实现分布式锁。

Redis在分布式锁方面的性能要高于zookeeper,同时他也存在他的缺点,我们之后会分析。

给个[在看],是对IT老哥最大的支持

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值