面试准备-- zookeeper实现分布式锁

本章学习需要先安装 zookeeper。安装教程在另一篇博客:
https://blog.csdn.net/weixin_41622183/article/details/90714190

什么是分布式锁
以前在项目较小时,单机即可处理很多业务。但随着用户增长,单机已经无法满足当今业务。解决方案可能是上集群,但是在多个 JVM 中都有同一个变量。假设多个请求分到不同的 JVM 中,都对变量进行修改,这就造成变量可能会的不正确。

为了防止分布式系统中的多个进程之间相互干扰,需要一个分布式协调技术,这个技术便是分布式锁

分布式锁和我们平常使用的锁类似

  • 排他,只允许一个线程占用
  • 可重入性,不可重入会造成死锁
  • 具备失效机制,万一执行过程崩溃,也会有自动过期机制

zookeeper 分布式锁原理
zookeeper 实现分布式锁是基于 zookeeper 数据存储结构,zookeeper 的数据存储结构类似于一棵树。其中的节点叫 Znode。

Znode 有四种类型,分别为 持久节点(PERSISTENT)持久节点顺序节点(PERSISTENT_SEQUENTIAL)临时节点(EPHEMERAL)临时顺序节点(EPHEMERAL_SEQUENTIAL)

而 zookeeper 实现分布式锁原理是依据临时顺序节点(EPHEMERAL_SEQUENTIAL) 来实现的。下面我们来学习一个 zookeeper 是如何利用临时顺序节点实现分布式锁的。

首先,我们创建一个持久节点 Lock,当客户端想要拿到锁时,需要创建一个临时节点 lock1。


客户端拿锁

这样,client-1 创建了临时节点 lock1 并拿到了锁。这时,假设又来了个 client-2 想要拿锁。

锁被占有

client-2 也在 Lock 下创建了临时节点 lock2,遍历 Lock 判断自己的 lock-2 前面是否前面是否还要节点,如果没有,说明自己是第一个,就顺利拿锁。如果有则表明锁被人拿了,client-2 将会注册一个 Watcher去监听 lock1 是否存在。

这时,又来了一个 client-3 想要拿锁,在 Lock 下创建了一个 lock3 节点,结果也发现自己不是最前面的一个。便会注册一个 Watcher 去监听上一个节点,也就是 lock2 判断其是存在。

client-3 拿锁失败

client-1 的业务执行完毕,可以释放锁了。执行完毕后,client-1 调用节点删除的指令,将临时节点 lock1 删除。


准备状态
client-1 释放锁

由于 client-2 一直在监听着 lock1,当 lock1 释放锁删除节点后,顺理成章的拿到了锁。而 lock3 还在监听着 lock2,等着它释放锁。


client-2 拿到锁

上面是正常业务执行完毕的情况下释放锁。假设,执行到中途,客户端崩溃了,与 zookeeper 断开了连接,又会如何呢?

临时节点的特性:仅当创建者会话有效时才得以保存,如果客户端崩溃了,那创建的会话是无效的,那样与客户端相关联的节点也会被删除。

client-2 崩了

监听 lock2 的客户端 client-3 发现节点 lock2 被删除,那样他也可以拿到锁了。


client-3 拿锁

最后,client-3 也释放锁,整个分布式锁原理就这样结束了。


在这里插入图片描述

使用 zookeeper 实现分布式锁
学习完了 zookeeper 分布式锁原理,下面们我们来使用 zookeeper 实现实现实现分布式锁。为了快速示例,这里使用 springboot 来进行编写。

依赖包

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

yml 配置文件

# zookeeper 地址
zookeeper.server = 192.168.79.128:2181,192.168.79.128:2182,192.168.79.128:2183
#锁文件路径
zookeeper.lockPath=/springboot/test
#重试间隔时间
zookeeper.elapsedTimeMs=5000
# session超时时间
zookeeper.sessionTimeoutMs=60000
#重试次数
zookeeper.retryCount=5
# 连接超时时间
zookeeper.connectionTimeoutMs=5000

zookeeper 配置

/**
 * @author Gentle
 * @date 2019/05/29 : 20:37
 */
@Component
@ConfigurationProperties(prefix = "zookeeper")
@Data
public class ZookeeperProperties {
    /**
     * zookeeper 地址:ip
     */
    private String server;
    /**
     * 加锁路径
     */
    private String lockPath;
    /**
     * session超时时间
     */
    private Integer sessionTimeoutMs;
    /**
     * 连接超时时间
     */
    private Integer connectionTimeoutMs;
    /**
     * 重试次数
     */
    private Integer retryCount;
    /**
     * 重试间隔时间
     */
    private  Integer elapsedTimeMs;
}

/**
 * @author Gentle
 * @date 2019/05/29 : 20:43
 */
@Component
public class ZookeeperConfig {
    @Autowired
    ZookeeperProperties zookeeperProperties;

    /**
     *  配置 Zookeeper 客户端,构建连接
     * @return CuratorFramework 对象
     */
    @Bean
    public CuratorFramework curatorFramework() {
        CuratorFramework curatorFramework = CuratorFrameworkFactory.newClient(
                zookeeperProperties.getServer(),
                zookeeperProperties.getSessionTimeoutMs(),
                zookeeperProperties.getConnectionTimeoutMs(),
                new RetryNTimes(zookeeperProperties.getRetryCount(), zookeeperProperties.getElapsedTimeMs()));
        curatorFramework.start();
        return curatorFramework;
    }
    /**
     * @param curatorFramework 分布式锁对象
     * @return
     */
    @Bean
    public InterProcessMutex interProcessMutex(CuratorFramework curatorFramework){
        return new InterProcessMutex(curatorFramework,zookeeperProperties.getLockPath());
    }
}

笔者编写了一个注解的方式进行解耦。

/**
 * 分布式做注解
 * @author Gentle
 * @date 2019/05/30 : 17:53
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LockAnnotation {
    /**
     * @return 锁等待时间
     */
    long time() default 20;
    /**
     * 时间类型 可选秒,毫秒,时等
     * @return
     */
    TimeUnit util() default TimeUnit.SECONDS;
}

AOP 拦截器

/**
 * 分布式锁注解
 * @author Gentle
 * @date 2019/05/30 : 17:48
 */
@Aspect
@Order(5)
@Component
public class LockIntercept {
    @Autowired
    private InterProcessMutex interProcessMutex;

    @Around("@annotation(lockAnnotation)")
    public void lockHandler(ProceedingJoinPoint joinPoint, LockAnnotation lockAnnotation ){
        //判断是否拿到锁
        boolean acquire=false;
        try {
            //自定义时间
            acquire  = interProcessMutex.acquire(lockAnnotation.time(), lockAnnotation.util());
            //拿锁成功则进行业务
            if (acquire){
                Object proceed = joinPoint.proceed();
            }
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }finally {
            //拿到锁的才进行释放
            if (acquire){
                try {
                    interProcessMutex.release();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

业务接口和实现类
这里的业务仅仅只做一个计数。

/**
 * @author Gentle
 * @date 2019/05/29 : 23:57
 */
public interface ZookeeperLockService {

    /**
     * 业务
     * @return
     * @throws Exception
     */
    Boolean lockAcquireTimeOut() throws Exception;
}

/**
 * @author Gentle
 * @date 2019/05/29 : 23:57
 */
@Service
public class ZookeeperLockServiceImpl implements ZookeeperLockService {

    @Autowired
    CuratorFramework curatorFramework;
    /**
     * 计数
     */
    int a =0;
    /**
     * 计数业务
     * @return false
     */
    @Override
    //分布式锁注解
    @LockAnnotation
    public Boolean lockAcquireTimeOut() {
        System.out.println(a++);
        return false;
    }
}

测试类

import com.gentle.service.ZookeeperLockService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

@RunWith(SpringRunner.class)
@SpringBootTest
public class GentleApplicationTests {

    @Autowired
    ZookeeperLockService zookeeperLockService;

    /**
     * 200 个线程测试分布式锁
     * @throws InterruptedException
     */
    @Test
    public void contextLoads() throws InterruptedException {
        //闭锁
        CountDownLatch countDownLatch = new CountDownLatch(200);
        ExecutorService executorService = Executors.newCachedThreadPool();
        //200 个线程进行测试
        for (int i = 0; i <200; i++) {
            executorService.submit(() -> {
                        try {
                            //调用业务
                            zookeeperLockService.lockAcquireTimeOut();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        countDownLatch.countDown();
                    }
            );
        }
        //等待完成
        countDownLatch.await();
    }
}

测试结果:

在这里插入图片描述

代码已经提交至 Gitee。
Gitee:
https://gitee.com/reway_wen/springboot-learn/tree/master/zookeeper-lock-demo

GitHub 较为凌乱,更新完毕后会在修改!

总结:
zookeeper 分布式锁相对来说还是比较简单的,可能在性能方面比 Redis 实现的分布式锁要差一些(频繁创建和删除节点),但胜在 zookeeper 的实现比较简单。当然,我们也学习了 zookeeper 实现分布式锁的原理,这一点很重要–面试经常问。

有兴趣的同学可以关注公众号,一起干起来!


在这里插入图片描述
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容