Zookeeper实现分布式锁


参考蚂蚁课堂

1.Zookeeper实现事件监听通知

zookeeper可以实现对节点不同行为进行监听比如说之前payment-service下面有两个节点一个是8080一个是8081。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k9VWIowr-1649339739710)(C:\Users\MICROSOFT\Pictures\博客图片\zookeeper\服务注册与发现\注册成功节点.png)]

我对8080进行监听,然后我把8080删除然后控制台就会提醒我8080被删了。这就是对事件的监听。下面我们来看一下实现的代码和最后的结果。

public class Test006 {
    //参数1 连接地址
    private static final String ADDRES = "192.168.247.3:2181";
    // 参数2 zk超时时间
    private static final int TIMEOUT = 5000;
    // 计数器
    private static CountDownLatch countDownLatch = new CountDownLatch(1);

    public static void main(String[] args) {

        // 创建了一个父节点 /mayikt-service/8080|8081
        // 1.创建我们的zk连接
        ZkClient zkClient = new ZkClient(ADDRES, TIMEOUT);
        String parentPath = "/payment-service";
        // 2.监听节点的value值是否发生变化
        zkClient.subscribeDataChanges(parentPath + "/8080", new IZkDataListener() {
            @Override
            // 节点的内容是否发生变化
            public void handleDataChange(String s, Object o) throws Exception {
                System.out.println("s:" + s + ",o:" + o);
            }

            @Override
            // 监听该节点是否被删除
            public void handleDataDeleted(String s) throws Exception {
                System.out.println("s被删除:" + s);
            }
        });

        while (true) {

        }


    }
}

首先我们先建立zkClient指定好,IP地址和端口号,然后指明要监听哪个节点这里是payment-service下的8080节点。然后我们删除这个节点最后结果如下图所示。

在这里插入图片描述

在这里插入图片描述

2.分布式锁的概念

2.1分布式锁应用场景

假设有这样一个场景比如说有一个业务逻辑需要定时执行, 我们就可以弄一个定时任务调度,定时执行该业务逻辑,但是假如说我要部署一个集群,也就是两台服务器jar包代码业务逻辑都是一样的,那么在同一时刻这个业务逻辑可能被执行两遍,这完全没有必要,有的时候还可能出现问题,所以我们就可以用分布式锁来对他们进行限制,保证同一时刻只有一台服务器可以拿到锁执行业务逻辑,其他服务器没拿到锁进入阻塞。分布式锁是相对于JVM的,保证在多个JVM中只能有一个JVM执行。平时说的锁比如说lock是相对于线程的。

2.2实现方案

1.基于数据库实现分布式锁

2.基于Redis实现分布式锁

3.基于zk实现分布式锁

4.基于redisson实现分布式锁

2.3基于zookeeper实现分布式锁的思路

2.3.1创建分布式锁的原理

1.多个Jvm同时在Zookeeper上创建相同的临时节点(lockPath)。

2.因为临时节点路径保证唯一性,只要谁能够创建成功谁就能够获取锁,就可以开始执行业务逻辑;同时只要这个会话结束就节点就会消失,这样下一轮其他Jvm也有获取锁的机会。

3.如果节点已经给其他请求创建的话,当前的请求实现等待。

2.3.2释放锁的原理

因为我们采用临时节点,当前节点成功,表示获取锁成功;正常执行完业务逻辑之后调用Session关闭连接方法,当前节点会删除。其他正在等待请求的Jvm采用事件监听,如果当前节点被删除的话,又重新进入到获取锁的流程;

综上所述:临时节点+事件通知 = zk分布式锁。

3. zk分布式锁的实现

首先我们可以将失信分布式锁的一些必要的步骤抽象出来,这样的话我们就不仅可以用zookeeper来实现,也可以用其他的比如redis,数据库之类的来实现分布式锁。

public interface Lock {

    /**
     * 获取锁
     */
    public void getLock();

    /**
     * 释放锁
     */
    public void unLock();

}
abstract class AbstractTemplzateLock implements Lock {


    @Override
    public void getLock() {
        // 模版方法 定义共同抽象的骨架
        if (tryLock()) {
            System.out.println(">>>" + Thread.currentThread().getName() + ",获取锁成功");
        } else {
            // 开始实现等待
            waitLock();// 事件监听
            // 重新获取
            getLock();
        }


    }

    /**
     * 获取锁
     *
     * @return
     */
    protected abstract boolean tryLock();

    /**
     * 等待锁
     *
     * @return
     */
    protected abstract void waitLock();

    /**
     * 释放锁
     *
     * @return
     */
    protected abstract void unImplLock();

    @Override
    public void unLock() {
        unImplLock();
    }
}

首先来一个Lock接口,里面有两个方法,也就是上面实现分布式锁思路中提到的获取锁和释放锁。然后使用一个抽象类来实现上述接口,首先是获取锁getLock(),这个获取锁有两种可能一种是获取锁成功,另一种是获取锁失败,如果获取锁失败的话,那么它将不断的等待,这个等待方法即waitLock(),这个方法实际上是一个事件监听,当zookeeper节点删除之后,waitLock()就会监听到,监听到了之后就认为上一个Jvm已经将分布式锁释放,所以我这个Jvm就会执行下面的getLock()方法重新获取锁。然后我们看看具体的zookeeper是如何实现这些方法的。

public class ZkTemplateImplLock extends AbstractTemplzateLock {
    //参数1 连接地址
    private static final String ADDRES = "192.168.247.3:2181";
    // 参数2 zk超时时间
    private static final int TIMEOUT = 5000;
    // 创建我们的zk连接
    private ZkClient zkClient = new ZkClient(ADDRES, TIMEOUT);
    /**
     * 共同的创建临时节点
     */
    private String lockPath = "/lockPath";
    private CountDownLatch countDownLatch = null;

    @Override
    protected boolean tryLock() {
        // 获取锁的思想:多个jvm同时创建临时节点,只要谁能够创建成功 谁能够获取到锁
        try {
            zkClient.createEphemeral(lockPath);
            return true;
        } catch (Exception e) {
//            // 如果创建已经存在的话
//            e.printStackTrace();
            return false;
        }
    }

    @Override
    protected void waitLock() {
        // 1.使用事件监听 监听lockPath节点是否已经被删除,如果被删除的情况下 有可以重新的进入到获取锁的权限
        IZkDataListener iZkDataListener = new IZkDataListener() {
@Override
            public void handleDataChange(String s, Object o) throws Exception {

            }

            @Override
            public void handleDataDeleted(String s) throws Exception {
                if (countDownLatch != null) {
                    countDownLatch.countDown();// 计数器变为0
                }

            }
        };
        zkClient.subscribeDataChanges(lockPath, iZkDataListener);
        // 2.使用countDownLatch等待
        if (countDownLatch == null) {
            countDownLatch = new CountDownLatch(1);
        }
        try {
            countDownLatch.await();// 如果当前计数器不是为0 就一直等待
        } catch (Exception e) {

        }
        // 3. 如果当前节点被删除的情况下,有需要重新进入到获取锁

    }

    @Override
    protected void unImplLock() {
        if (zkClient != null) {
            zkClient.close();
            System.out.println(Thread.currentThread().getName() + ",释放了锁>>>");
        }
    }
}

这个zkTemplateLock继承了上面的抽象类,然后重写了tryLock()方法,这个tryLock方法就是首先我创建一个临时节点,如果能创建成功返回true,如果失败则会抛出异常,返回false。然后就是下面的waitLock,waitLock是等待重新获取锁,它还包含了事件监听功能,我们通过countDownLatch来让他达到阻塞状态,当countDownLatch为空的时候创建一个countDownLatch默认值为1,如果之前节点没有被删除,也就是没释放锁的话countDownLatch一直就是1,也就是说他会一直阻塞,当那个节点被删除时,他会监听到然后将countDownLatch.countDown()减一然后就不会被阻塞,最终会退出waitLock方法,然后重新执行getLock()。释放锁就是把zk连接关了临时节点自动消失。

然后我们来做个测试。这里面我们用多线程去模拟多个Jvm,我们可以写一个订单Service类。

public class OrderService implements Runnable {
    private OrderNumGenerator orderNumGenerator = new OrderNumGenerator();

    private Lock lock = new ZkTemplzateImplLock();

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

    private void getNumber() {
        try {
            lock.getLock();
            Thread.sleep(50);
            String number = orderNumGenerator.getNumber();
            System.out.println(Thread.currentThread().getName() + ",获取的number:" + number);

            // 如果zk超时了,有做数据库写的操作统一直接回滚
        } catch (Exception e) {

        } finally {
            lock.unLock();
        }
    }
}

OrderService实现了Runnable接口,一会我们开多个线程让这个OrderService生成订单号,然后重写他的run方法,这个run方法里面是getNumber生成订单号,然后这个订单号由这个类来实现。

public class OrderNumGenerator {

    /**
     * 序号
     */
    private static int count;

    /**
     * 生成我们的时间戳 为订单号码
     *
     * @return
     */
    public String getNumber() {
        SimpleDateFormat simpt = new SimpleDateFormat("yyyy-MM-dd-OHH-mm-ss");
        try {

            Thread.sleep(30);
        } catch (Exception e) {

        }
        return simpt.format(new Date()) + "-" + ++count;
    }

}

实际上就是一个时间戳加上count计数器。在OrderService当中,getNumber这个函数里面首先要获取分布式锁,如果获取成功才会执行下面的生成订单号的业务逻辑。然后最终释放锁。我们开启Zookeeper来看一下运行结果。

在这里插入图片描述

我们能看到是我们想要的结果,每个线程都会获取锁然后执行业务逻辑打印出订单号,然后释放锁,紧接着下一个线程进来执行同样的流程。

4.如何防止死锁问题

假如说出现了这样一个场景,在执行业务逻辑的过程当中,业务逻辑是一个非常耗时的逻辑,这时候有可能拿到锁了,一直不释放锁,这就造成了死锁。我们可以这样解决设置一个超时时间达到一定的时间,直接释放zk连接,然后我们将数据库发生写操作的部分全部回滚。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

温JZ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值