分布式锁实现之Zookeeper

1,锁的的基本概念

开发中锁的概念并不陌生,通过锁可以实现在多个线程或多个进程间在争抢资源时,能够合理的分配置资源的所有权。在单体应用中我们可以通过 synchronized 或ReentrantLock 来实现锁。但在分布式系统中,仅仅是加synchronized 是不够的,需要借助第三组件来实现。比如一些简单的做法是使用 关系型数据行级锁来实现不同进程之间的互斥,但大型分布式系统的性能瓶颈往往集中在数据库操作上。为了提高性能得采用如Redis、Zookeeper之内的组件实现分布式锁。

  1. 共享锁 也称作只读锁,当一方获得共享锁之后,其它方也可以获得共享锁。但其只允许读取。在共享锁全部释放之前,其它方不能获得写锁;
    2.排他锁 也称作读写锁,获得排它锁后,可以进行数据的读写。在其释放之前,其它方不能获得任何锁。

2,基于zk实现分布式锁的原理图

获取锁的流程

3,主要代码示例

主要代码如下:.

// Lock实体类
package com.opensoul.zk.lock;

import java.io.Serializable;
/**
 * @Description: 锁节点实例类
 * @Author wgxu
 * @date 2020/10/18 12:12
 */
public class Lock implements Serializable {
    private static final long serialVersionUID = 1908357942011709844L;

    private String lockId;

    private String path;

    private boolean isActive;

    @Override
    public String toString() {
        return "Lock{" +
                "lockId='" + lockId + '\'' +
                ", path='" + path + '\'' +
                ", isActive=" + isActive +
                '}';
    }

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

    public String getLockId() {
        return lockId;
    }

    public void setLockId(String lockId) {
        this.lockId = lockId;
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public boolean isActive() {
        return isActive;
    }

    public void setActive(boolean active) {
        isActive = active;
    }
}

// An highlighted block
package com.opensoul.zk.lock;

import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.apache.log4j.Logger;

import java.io.Serializable;
import java.util.List;
import java.util.stream.Collectors;


/**
 *
 * @Description: 锁的逻辑实现类
 * @Author wgxu
 * @date 2020/10/18 12:12
 */
public class ZookeeperLock implements Serializable {
    private static final Logger logger = Logger.getLogger(ZookeeperLock.class);
    
    private static final long serialVersionUID = 635945547698890884L;
	//zk服务器参数
    private String server = "127.0.0.1:2181";
    private ZkClient zkClient;
    //固定持久节点
    private static final String rootPath = "/zk-lock";

    public ZookeeperLock() {
        logger.error("准备建立连接....................");
        zkClient = new ZkClient(server, 5000, 30000);
        logger.error("已经和服务器建立连接,准备创建根节点");
        buildRoot();
    }

    // 构建根节点
    public void buildRoot() {
        if (!zkClient.exists(rootPath)) {
            zkClient.createPersistent(rootPath);
        }
    }
    // 获取锁
    public Lock lock(String lockId, long timeout) {
        // 创建临时节点
        Lock lockNode = createLockNode(lockId);
        logger.debug("------"+Thread.currentThread().getId() + "------00000000号位创建临时节点:" + lockNode.getPath());
        lockNode = tryActiveLock(lockNode);// 尝试激活锁
        if (!lockNode.isActive()) {
            try {
                synchronized (lockNode) {
                    lockNode.wait(timeout); // 线程锁住
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        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) {
        logger.debug("------"+Thread.currentThread().getId() + "------11111111号位试图激活节点:" + lockNode.getPath());

        // 获取根节点下面所有的子节点
        List<String> list = zkClient.getChildren(rootPath)
                .stream()
                .sorted()
                .map(p -> rootPath + "/" + p)
                .collect(Collectors.toList());      // 判断当前是否为最小节点
        logger.debug("------"+Thread.currentThread().getId() + "------22222222号位获得root下的所有子节点:" + list);
        String firstNodePath = list.get(0);
        logger.debug("------"+Thread.currentThread().getId() + "------33333333号位获得root下的所有子节点中的最小节点:[" + lockNode.getPath() + "]");
        // 最小节点是不是当前节点
        if (firstNodePath.equals(lockNode.getPath())) {
            logger.debug("------"+Thread.currentThread().getId() + "------44444444号位获得root下的所有子节点中的最小节点:[" + firstNodePath + "] 位本节点自身,设置激活状态为[" + lockNode.getPath().equals(firstNodePath) + "]");
            lockNode.setActive(true);
        } else {
            String upNodePath = list.get(list.indexOf(lockNode.getPath()) - 1);
            logger.debug("------"+Thread.currentThread().getId() + "------55555555号位,当前节点与最小节点不一样,获取当前节点:[" + lockNode.getPath() + "]的上一个节点[" + upNodePath + "]");
            logger.debug("------"+Thread.currentThread().getId() + "------555555-1号位,当前节点与最小节点不一样,获取当前节点:[" + lockNode.getPath() + "]的上一个节点[" + upNodePath + "] 进行监听......");
            zkClient.subscribeDataChanges(upNodePath, new IZkDataListener() {

                @Override
                public void handleDataChange(String dataPath, Object data) throws Exception {

                }

                @Override
                public void handleDataDeleted(String dataPath) throws Exception {
                    // 事件处理 与心跳 在同一个线程,如果Debug时占用太多时间,将导致本节点被删除,从而影响锁逻辑。
                    logger.debug("------"+Thread.currentThread().getId() + "------555555-2号位,当前节点:[" + lockNode.getPath() + "]的上一个节点[" + upNodePath + "] 被监听到已经被删除了!");
                    logger.debug("节点删除:" + dataPath);
                    logger.debug("------"+Thread.currentThread().getId() + "------555555-3号位,再次试图激活当前节点:[" + lockNode.getPath() + "]");
                    Lock lock = tryActiveLock(lockNode);
                    logger.debug("------"+Thread.currentThread().getId() + "------555555-4号位,再次试图激活当前节点:[" + lockNode.getPath() + "]后,节点被激活");
                    synchronized (lockNode) {
                        logger.debug("------"+Thread.currentThread().getId() + "------555555-5号位,synchronized当前节点:[" + lockNode.getPath() + "]判断节点是否已经被激活["+lock.isActive()+"]");
                        if (lock.isActive()) {
                            logger.debug("------"+Thread.currentThread().getId() + "------555555-6号位,synchronized当前节点:[" + lockNode.getPath() + "]判断节点是否已经被激活["+lock.isActive()+"],激活完成,唤醒等待notify......");
                            lockNode.notify(); // 释放了
                        }
                    }
                    logger.debug("------"+Thread.currentThread().getId() + "------555555-7号位,解除监听当前节点:[" + lockNode.getPath() + "]的上一个个["+lock.isActive()+"],激活完成,唤醒等待notify......");
                    zkClient.unsubscribeDataChanges(upNodePath, this);//解除监听
                }
            });
        }
        logger.debug("------"+Thread.currentThread().getId() + "------4444444-1号位获得root下的所有子节点中的最小节点:[" + firstNodePath + "] 位本节点自身," +
                "设置激活状态为[" + lockNode.getPath().equals(firstNodePath) + "],获得锁后返回。return lockNode");
        return lockNode;
    }


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

    public ZkClient getZkClient() {
        return zkClient;
    }
}

测试类:.

package com.opensoal.zk.test;

import com.opensoul.zk.lock.Lock;
import com.opensoul.zk.lock.ZookeeperLock;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.apache.log4j.Logger;
import org.junit.Before;
import org.junit.Test;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;


public class LockTest {

    private static final Logger logger = Logger.getLogger(LockTest.class);
    ZookeeperLock zookeeperLock;
    static Long count = 0L;

    @Before
    public void init() {
        zookeeperLock = new ZookeeperLock();
    }

    /**
     * 模拟测试竞争锁
     * @throws InterruptedException
     * @throws IOException
     */
    @Test
    public void run() throws InterruptedException, IOException {
        // 写数字 0+100 =100
        File file = new File("e:/test.txt");
        if (!file.exists()) {
            file.createNewFile();
        }
        ExecutorService executorService = Executors.newCachedThreadPool();
        logger.info(file.getPath());
        for (int i = 0; i < 10; i++) {// 1000 个线程存在问题
            executorService.submit(() -> {
                                                //d:\test.txt
                Lock lock = zookeeperLock.lock(file.getPath(), 60 * 1000);
                logger.info(lock.getPath()+"------------------------------被激活获得锁");
                try {
                    String firstLine = Files.lines(file.toPath()).findFirst().orElse("0");
                    int count = Integer.parseInt(firstLine);
                    count++;
                    Files.write(file.toPath(), String.valueOf(count).getBytes());
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    zookeeperLock.unlock(lock);
                }
            });
        }
        executorService.shutdown();
        executorService.awaitTermination(200, TimeUnit.SECONDS);
        String firstLine = Files.lines(file.toPath()).findFirst().orElse("0");
        logger.info(firstLine);
    }

    /**
     * 测试获取锁
     * @throws InterruptedException
     */
    @Test
    public void getLockTest() throws InterruptedException {
        zookeeperLock.createLockNode("E:\test.txt");
        Lock lock = zookeeperLock.lock("opensoul", 60 * 1000);
        logger.info("------"+Thread.currentThread().getId() + "------成功获取锁");
        Thread.sleep(Long.MAX_VALUE);
        assert lock != null;
    }
}

4,项目源码git地址:

https://gitee.com/ygbccqzxxdr/zookeeper-lock.git代码如有错误之处,敬请指正。

我假装老成,人们就传我老成; 我假装是个懒汉,人们就谣传我是个懒惰虫; 我充阔,人们都以为我是阔佬; 我故作冷淡,人们就说我是个无情的家伙; 然而当我真的难过万分,不禁发出痛苦呻吟的时候,人们却认为我无病呻吟。 世上本就不存在感同身受,笑容可以瞒过别人,心痛却瞒不过自己。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值