zookeeper 分布式锁

分布式锁的概念,大家应该都已经理解,在此不会细讲。

分布式锁简单来说就是服务器集群环境下出现用户高并发访问同一个资源时,对该资源访问进行加锁等待,以保证资源的准确性。

zookeeper的分布式锁是并发的多线程通过循环的请求创建zk节点来竞争锁的占有权,待取得占有权后,其他线程进入等待。待释放占有权后,其他线程再进行循环竞争。

 

本编文章,主要讲解zk分布式锁,如何使用,具体逻辑还需根据实际场景进行调整。

代码是在本地建设,为了方便测试,所以里面都是静态方法。真正的开发环境都是基于webservlet或微服务工程,使用bean的方式进行类对象或者方法的调用。大家可以根据自己的工程业务做zk分布式锁的封装。

重点提醒下:如果使用zk的watcher监听通知,节点创建后并瞬间删除,zkServer将会监听失败。因为zkServer的监听有延迟,当执行监听的时候,他发现并无该节点的stat信息,故不执行监听。

 

1.客户端创建

 zk是支持集群的,所以这里两种客户端形式,代码操作是一样的,唯有连接地址略有差异。

package com.qy.zk.lock;

import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.RetryNTimes;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ZkLockClient {
    
        private static final Logger log = LoggerFactory.getLogger(ZkLockClient.class);
    
        //集群节点
        public static final String zkServerClusterConnect = "192.168.159.129:2181,192.168.159.129:2182,192.168.159.129:2183";
        
        //单一节点
        public static final String zkServerSingleConnect = "192.168.159.129:2181";
        
        
        /**
         * @author 七脉
         * 描述:获取CuratorFramework的客户端
         * @return
         */
        public static CuratorFramework client(){
            log.info("创建CuratorFramework客户端");
            int sessionTimeoutMs = 10000;//会话超时时间
            int connectionTimeoutMs = 3000;//初次链接超时时间
            int n = 3;//重试链接次数
            int sleepMsBetweenRetries = 3000;//每次重试连接间隔毫秒数
            
            //RetryPolicy重试显现策略有很多,具体可以查看RetryPolicy的每个实现类进行测试。
            RetryPolicy retryPolicy = new RetryNTimes(n, sleepMsBetweenRetries);
            
            //创建客户端
            CuratorFramework curatorFramework = CuratorFrameworkFactory.builder().connectString(zkServerClusterConnect).connectionTimeoutMs(connectionTimeoutMs).sessionTimeoutMs(sessionTimeoutMs).retryPolicy(retryPolicy).namespace("lockspace").build();
            return curatorFramework;
        }
}

 

2.分布式锁方法

分布式锁:上锁、释放锁,这里使用的是创建节点为上锁,删除节点为释放锁。使用ZK节点监听事件做锁的相关通知

  

package com.qy.zk.lock;

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

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCache.StartMode;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author 七脉
 * 描述:zk分布式锁形式,通过不停创建节点,成功获取到锁,失败获取锁失败
 */
public class ZkLock {
    private static final Logger log = LoggerFactory.getLogger(ZkLock.class);
    
    //初始化同步线程锁
    public static CountDownLatch countDownLatch = new CountDownLatch(1);
    
    
    /**
     * @author 七脉
     * 描述:获取锁,成功则处理业务,然后释放锁
     * @param client
     * @param path
     * @return
     */
    public static boolean getLock(CuratorFramework client,String lockPath){
        //30秒内获取不到锁,则放弃
        long timeout = System.currentTimeMillis()+30000;
        while(timeout>=System.currentTimeMillis()){
            try {
                //创建节点,成功则获取到锁
                client.create().creatingParentContainersIfNeeded().withMode(CreateMode.EPHEMERAL).withACL(Ids.OPEN_ACL_UNSAFE).forPath(lockPath);
                log.info("获取分布式锁成功。。。。{}", lockPath);
                return true;
            } catch (Exception e) {
                //其实下面这种方式,我并不会真正用到分布式锁的方法中,因为通过事件通知做countdown是有延迟的
                log.info("获取分布式锁失败,继续获取 {} 锁。。。。", lockPath);
                //竞争失败,将CountDownLatch值归1,等待下次开始竞争锁
                if(countDownLatch.getCount()==0){
                    countDownLatch = new CountDownLatch(1);
                }
                //等待,设置超时
                try {
                    //使用CountDownLatch一定要注意活锁,不然锁已经释放了,但count值出错,线程将永久卡死下去
                    countDownLatch.await(1, TimeUnit.SECONDS);
                } catch (InterruptedException e1) {
                    log.info("countDownLatch.await出错:{}",e1);
                    continue;
                }
            }
        }
        log.info("获取锁超时,{}",lockPath);
        return false;
    }
    
    /**
     * @author 七脉
     * 描述:释放锁,就是将临时节点删除,重新让其他线程竞争获取锁权限
     * @param client
     * @param path
     * @return
     * @throws Exception 
     */
    public static boolean freeLock(CuratorFramework client,String lockPath) {
        try{
            Stat stat = client.checkExists().forPath(lockPath);
            if(null!=stat){
                client.delete().guaranteed()//就算网络遇见抖动,只要连接成功,也会保证删除
                .deletingChildrenIfNeeded()//递归删除子节点
                .withVersion(stat.getVersion()).forPath(lockPath);
            }
            log.info("锁:{},释放成功。", lockPath);
            return true;
        }catch(Exception e){
            log.info("锁:{},释放失败。{}", lockPath, e);
        }
        return false;
    }
    
    /**
     * @author 七脉
     * 描述:添加父节点监听通知,监听子节点操作
     * @param client
     * @throws Exception 
     */
    public static PathChildrenCache addPathChildrenCacheListener(CuratorFramework client, String parentPath, String lockPath) {
        log.info("添加父节节点监听事件:{}", parentPath);
        
        PathChildrenCache pathChildrenCache = new PathChildrenCache(client, parentPath, true);
        //StartMode.BUILD_INITIAL_CACHE同步初始化缓存数据
        try {
            pathChildrenCache.start(StartMode.BUILD_INITIAL_CACHE);
        } catch (Exception e) {
            log.info("{}",e);
        }
        
        //添加节点监听事件
        pathChildrenCache.getListenable().addListener(new ZkLockWatcher(lockPath));
        
        //关闭则不再监听
        //pathChildrenCache.close();
        
        return pathChildrenCache;
    }
}

 

3.锁监听通知

使用PathChildrenCacheListener监听子节点做锁的操作通知业务

package com.qy.zk.lock;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @author 七脉
 * 描述:分布式锁监听事件处理
 */
public class ZkLockWatcher implements PathChildrenCacheListener{
    
    private static final Logger log = LoggerFactory.getLogger(ZkLockWatcher.class);
    
    /**锁节点**/
    private String lockPath;
    
    /**
     * 业务处理
     */
    public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
        System.out.println("接收到PathChildrenCacheListener监听事件");
        log.info("接收到PathChildrenCacheListener监听事件,节点:{},事件类型:{}", event.getData().getPath(), event.getType());
        //如果监听到该锁释放,取消countDownLatch线程等待
        if(event.getData().getPath().endsWith(lockPath) && event.getType()==PathChildrenCacheEvent.Type.CHILD_REMOVED){
            ZkLock.countDownLatch.countDown();
        }
    }
    
    public ZkLockWatcher(String lockPath) {
        this.lockPath = lockPath;
    }

    public String getLockPath() {
        return lockPath;
    }

    public void setLockPath(String lockPath) {
        this.lockPath = lockPath;
    }

    public static Logger getLog() {
        return log;
    }
    
}

 

4.测试

创建多个线程,同时访问统一资源,查看最后资源是否出错

  

package com.qy.zk.lock;

import org.apache.curator.framework.CuratorFramework;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Test {
    private static final Logger log = LoggerFactory.getLogger(Test.class);
    
    public static CuratorFramework client = ZkLockClient.client();
    
    //父节点
    public static final String prentLockPath = "/order";
    
    //锁节点
    public static final String lockPath = prentLockPath+"/order-pen";
    
    //假设有10个钢笔
    public static int penCount = 10;
    
    public static void main(String[] args) throws InterruptedException {
        //开启客户端连接
        client.start();
        ZkLock.addPathChildrenCacheListener(client, prentLockPath, lockPath);
        Thread t1 = new Thread(new Runnable() {
            public void run() {
                buyPen(6,client,lockPath);
            }
        });
        
        Thread t2 = new Thread(new Runnable() {
            public void run() {
                buyPen(4,client,lockPath);
            }
        });
        
        Thread t3 = new Thread(new Runnable() {
            public void run() {
                buyPen(5,client,lockPath);
            }
        });
        
        Thread t4 = new Thread(new Runnable() {
            public void run() {
                buyPen(1,client,lockPath);
            }
        });
        t2.start();
        t1.start();
        t3.start();
        t4.start();
        
        //client.close();
    }
    
    /**
     * @author 七脉
     * 描述:测试购买流程
     * @param count
     * @param client
     * @param lockPath
     * @throws InterruptedException 
     */
    public static void buyPen(int count,CuratorFramework client,String lockPath) {
        boolean lock = ZkLock.getLock(client, lockPath);
        if(lock){
            if(penCount>=count){
                penCount = penCount - count;
                log.info("购买成功,购买数量:{},剩余数量{}",count,penCount);
            }else{
                log.info("购买失败,购买数量:{},剩余数量{}",count,penCount);
            }
            //看我代码的同学,请注意这里,zk在创建节点获取到锁后,瞬间再删除节点释放锁,watcher通知是监听不到的
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            ZkLock.freeLock(client, lockPath);
        }
    }
}

输出:

注意:zk的监听通知是有延迟的,打印顺序不会完全按照代码节奏执行。

创建CuratorFramework客户端 
添加父节节点监听事件:/order 
获取分布式锁成功。。。。/order/order-pen 
购买成功,购买数量:5,剩余数量5 
获取分布式锁失败,继续获取 /order/order-pen 锁。。。。 
获取分布式锁失败,继续获取 /order/order-pen 锁。。。。 
获取分布式锁失败,继续获取 /order/order-pen 锁。。。。 
接收到PathChildrenCacheListener监听事件
接收到PathChildrenCacheListener监听事件,节点:/order/order-pen,事件类型:CHILD_ADDED 
获取分布式锁失败,继续获取 /order/order-pen 锁。。。。 
获取分布式锁失败,继续获取 /order/order-pen 锁。。。。 
获取分布式锁失败,继续获取 /order/order-pen 锁。。。。 
接收到PathChildrenCacheListener监听事件
接收到PathChildrenCacheListener监听事件,节点:/order/order-pen,事件类型:CHILD_REMOVED 
锁:/order/order-pen,释放成功。 
获取分布式锁成功。。。。/order/order-pen 
购买失败,购买数量:6,剩余数量5 
获取分布式锁失败,继续获取 /order/order-pen 锁。。。。 
获取分布式锁失败,继续获取 /order/order-pen 锁。。。。 
接收到PathChildrenCacheListener监听事件
接收到PathChildrenCacheListener监听事件,节点:/order/order-pen,事件类型:CHILD_ADDED 
获取分布式锁失败,继续获取 /order/order-pen 锁。。。。 
获取分布式锁失败,继续获取 /order/order-pen 锁。。。。 
接收到PathChildrenCacheListener监听事件
接收到PathChildrenCacheListener监听事件,节点:/order/order-pen,事件类型:CHILD_REMOVED 
锁:/order/order-pen,释放成功。 
获取分布式锁成功。。。。/order/order-pen 
购买成功,购买数量:4,剩余数量1 
获取分布式锁失败,继续获取 /order/order-pen 锁。。。。 
接收到PathChildrenCacheListener监听事件
接收到PathChildrenCacheListener监听事件,节点:/order/order-pen,事件类型:CHILD_ADDED 
获取分布式锁失败,继续获取 /order/order-pen 锁。。。。 
接收到PathChildrenCacheListener监听事件
接收到PathChildrenCacheListener监听事件,节点:/order/order-pen,事件类型:CHILD_REMOVED 
锁:/order/order-pen,释放成功。 
获取分布式锁成功。。。。/order/order-pen 
购买成功,购买数量:1,剩余数量0 
接收到PathChildrenCacheListener监听事件
接收到PathChildrenCacheListener监听事件,节点:/order/order-pen,事件类型:CHILD_ADDED 
接收到PathChildrenCacheListener监听事件
接收到PathChildrenCacheListener监听事件,节点:/order/order-pen,事件类型:CHILD_REMOVED 
锁:/order/order-pen,释放成功。

5.pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.1.RELEASE</version>
    </parent>
    
    <groupId>com.qy.learn</groupId>
    <artifactId>qy-learn-zk-lock</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>
    
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
        <maven.test.skip>true</maven.test.skip>
        <java.version>1.8</java.version>
        <spring.boot.version>2.0.1.RELEASE</spring.boot.version>
        <qy.code.version>0.0.1-SNAPSHOT</qy.code.version>
    </properties>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <!-- 不使用springboot默认log -->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>
        
        <!-- https://mvnrepository.com/artifact/org.apache.zookeeper/zookeeper -->
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.4.12</version>
            <!-- 排除冲突jar -->
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-log4j12</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        
        <!-- https://mvnrepository.com/artifact/org.apache.curator/curator-framework -->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>4.1.0</version>
        </dependency>
        
        <!-- https://mvnrepository.com/artifact/org.apache.curator/curator-recipes -->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>4.1.0</version>
        </dependency>
        
        
    </dependencies>
    
    <repositories>
        <repository>
            <id>nexus-aliyun</id>
            <name>Nexus aliyun</name>
            <url>http://maven.aliyun.com/nexus/content/groups/public</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>nexus-aliyun</id>
            <name>Nexus aliyun</name>
            <url>http://maven.aliyun.com/nexus/content/groups/public</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>
    
    
    <build>
        <plugins>
            <!-- 要将源码放上去,需要加入这个插件 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-source-plugin</artifactId>
                <configuration>
                    <attach>true</attach>
                </configuration>
                <executions>
                    <execution>
                        <phase>compile</phase>
                        <goals>
                            <goal>jar</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>