前一段时间,我发表了一篇关于Redis实现分布式锁 分布式环境下利用Redis实现分布式锁,今天我带领大家熟悉用zookeeper实现分布式锁。

在学习分布式锁之前,让我们想一想,在什么业务场景下会用到分布式锁以及设计分布式锁要注意什么?

分布式锁介绍

1、在什么业务场景中会使用到分布式锁

当多个客户端访问服务器上同一个资源的时候,需要保证数据的一致性,比如秒杀系统,举个栗子:

某件商品在系统中的数量是5件,当秒杀时间到来,会有大量的用户抢购这件商品,瞬间会产生非常大的并发。正常的购买流程是:

step1、用户下单

step2、判断商品数量是否足够

step3、如果足够,库存--

step4、如果库存不够,秒杀失败。

假设此时商品只剩余一件,用户A对商品下单,商品数足够,下单成功,系统还没有来得及减库存,用户B也对同一件商品下单,此时商品数仍为1,最后导致系统会库存减两次,导致商品超卖现象。此时就需要对用户下单-->减库存的这一步操作进行加锁,使操作成为原子操作。在单机、单进程环境下,使用JDK的ReentrantLcok或者synchronized完全足够,但由于秒杀系统并发量极大,单机承受不了这样的压力极易宕机,此时就需要多台服务器、多进程支撑起这个业务,单机下的ReentrantLcok或者synchronized在此处毫无用武之地,此时就需要一把分布式锁来保证某个时间段只有一个用户访问共享资源。

2、分布式锁的注意事项

a、高效的获取锁和释放锁

b、在网络不稳定、中断、宕机情况下要自动释放锁,防止自锁

c、有阻塞锁的特性,即使没有获取锁,也会阻塞等待

d、具备非阻塞锁特性,即没有获取到锁,则直接返回获取锁失败

e、具备可重入行,同一线程可多次获得锁

zookeeper实现分布式锁

对于zookeeper,在此就不多介绍,我们可以利用zk的顺序临时节点这一特性来实现分布式锁。思路如下:

1、获取锁时,在zk的目录下创建一个节点,判断该节点的需要在其兄弟节点中是否是最小的,若是最小的,则获取锁成功。

2、若不是最小的,则锁已被占用,需要对比自己小的节点注册监听器,如果锁释放,监听到释放锁事件,判断此时节点在其兄弟节点是不是最小的,如果是,获取锁。

下面我来介绍Apache 开源的curator 实现 Zookeeper 分布式锁以及源码分析。

首先,导入相关的maven

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

再看main方法

public class Application {

	private static String address = "192.168.1.100:2181";
	public static void main(String[] args) {
		//1、重试策略:初试时间为1s 重试3次
		RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
		//2、通过工厂创建连接
		CuratorFramework client = CuratorFrameworkFactory.newClient(address, retryPolicy);
		//3、开启连接
		client.start();
		//4 分布式锁
		final InterProcessMutex mutex = new InterProcessMutex(client, "/curator/lock");
		//读写锁
		//InterProcessReadWriteLock readWriteLock = new InterProcessReadWriteLock(client, "/readwriter");
		ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
		for (int i = 0; i < 5; i++) {
			fixedThreadPool.submit(new Runnable() {
				@Override
				public void run() {
					boolean flag = false;
					try {
						//尝试获取锁,最多等待5秒
						flag = mutex.acquire(5, TimeUnit.SECONDS);
						Thread currentThread = Thread.currentThread();
						if(flag){
							System.out.println("线程"+currentThread.getId()+"获取锁成功");
						}else{
							System.out.println("线程"+currentThread.getId()+"获取锁失败");
						}
						//模拟业务逻辑,延时4秒
						Thread.sleep(4000);
					} catch (Exception e) {
						e.printStackTrace();
					} finally{
						if(flag){
							try {
								mutex.release();
							} catch (Exception e) {
								e.printStackTrace();
							}
						}
					}
				}
			});
		}
	}
}

上面代码展示得知,

public boolean acquire(long time, TimeUnit unit)

方法是获得锁的方法,参数是自旋的时间,所以我们分析这个方法的源码。

public boolean acquire(long time, TimeUnit unit) throws Exception {
    return this.internalLock(time, unit);
}

可见,acquire调用的是internalLock方法

private boolean internalLock(long time, TimeUnit unit) throws Exception {
        Thread currentThread = Thread.currentThread();
        //获得当前线程的锁
        InterProcessMutex.LockData lockData = (InterProcessMutex.LockData)this.threadData.get(currentThread);
        //如果锁不为空,当前线程已经获得锁,可重入锁,lockCount++
        if (lockData != null) {
            lockData.lockCount.incrementAndGet();
            return true;
        } else {
            //获取锁,返回锁的节点路径
            String lockPath = this.internals.attemptLock(time, unit, this.getLockNodeBytes());
            if (lockPath != null) {
                //向当前锁的map集合添加一个记录
                InterProcessMutex.LockData newLockData = new InterProcessMutex.LockData(currentThread, lockPath);
                this.threadData.put(currentThread, newLockData);
                return true;
            } else {
                return false;//获取锁失败
            }
        }
    }

下面是threadData的数据结构,是一个Map结构,key是当前线程,value是当前线程和锁的节点的一个封装对象。

private final ConcurrentMap<Thread, InterProcessMutex.LockData> threadData;

private static class LockData {
        final Thread owningThread;
        final String lockPath;
        final AtomicInteger lockCount;

        private LockData(Thread owningThread, String lockPath) {
            this.lockCount = new AtomicInteger(1);
            this.owningThread = owningThread;
            this.lockPath = lockPath;
        }
}

由internalLock方法可看到,最重要的方法是attemptLock方法

String lockPath = this.internals.attemptLock(time, unit, this.getLockNodeBytes());

 

String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception {
        long startMillis = System.currentTimeMillis();
        //将等待时间转化为毫秒
        Long millisToWait = unit != null ? unit.toMillis(time) : null;
        byte[] localLockNodeBytes = this.revocable.get() != null ? new byte[0] : lockNodeBytes;
        //重试次数
        int retryCount = 0;
        String ourPath = null;
        boolean hasTheLock = false;
        boolean isDone = false;

        while(!isDone) {
            isDone = true;

            try {
                //在当前path下创建临时有序节点
                ourPath = this.driver.createsTheLock(this.client, this.path, localLockNodeBytes);
                //判断是不是序号最小的节点,如果是返回true,否则阻塞等待
                hasTheLock = this.internalLockLoop(startMillis, millisToWait, ourPath);
            } catch (NoNodeException var14) {
                if (!this.client.getZookeeperClient().getRetryPolicy().allowRetry(retryCount++, System.currentTimeMillis() - startMillis, RetryLoop.getDefaultRetrySleeper())) {
                    throw var14;
                }

                isDone = false;
            }
        }
        //返回当前锁的节点路径

        return hasTheLock ? ourPath : null;
    }

下面来看internalLockLoop方法,判断是不是最小节点的方法

private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception {
        boolean haveTheLock = false;
        boolean doDelete = false;

        try {
            if (this.revocable.get() != null) {
                ((BackgroundPathable)this.client.getData().usingWatcher(this.revocableWatcher)).forPath(ourPath);
            }
            //自旋
            while(this.client.getState() == CuratorFrameworkState.STARTED && !haveTheLock) {
                //获得所有子节点
                List<String> children = this.getSortedChildren();
                String sequenceNodeName = ourPath.substring(this.basePath.length() + 1);
                PredicateResults predicateResults = this.driver.getsTheLock(this.client, children, sequenceNodeName, this.maxLeases);
                //判断是否是最小节点
                if (predicateResults.getsTheLock()) {
                    haveTheLock = true;
                } else {
                    //给比自己小的节点设置监听器
                    String previousSequencePath = this.basePath + "/" + predicateResults.getPathToWatch();
                    //同步,是为了实现公平锁
                    synchronized(this) {
                        try {
                            ((BackgroundPathable)this.client.getData().usingWatcher(this.watcher)).forPath(previousSequencePath);
                            //如果等待时间==null,一直阻塞等待
                            if (millisToWait == null) {
                                this.wait();
                            } else {
                                millisToWait = millisToWait - (System.currentTimeMillis() - startMillis);
                                startMillis = System.currentTimeMillis();
                                //等待超时时间
                                if (millisToWait > 0L) {
                                    this.wait(millisToWait);
                                } else {
                                    doDelete = true;//如果超时则删除锁
                                    break;
                                }
                            }
                        } catch (NoNodeException var19) {
                            ;
                        }
                    }
                }
            }
        } catch (Exception var21) {
            ThreadUtils.checkInterrupted(var21);
            doDelete = true;
            throw var21;
        } finally {
            if (doDelete) {
                //如果锁超时,删除锁
                this.deleteOurPath(ourPath);
            }

        }

        return haveTheLock;
    }

释放锁:

public void release() throws Exception {
        Thread currentThread = Thread.currentThread();
        InterProcessMutex.LockData lockData = (InterProcessMutex.LockData)this.threadData.get(currentThread);
        //当前线程没有获取锁,不能释放
        if (lockData == null) {
            throw new IllegalMonitorStateException("You do not own the lock: " + this.basePath);
        } else {
            int newLockCount = lockData.lockCount.decrementAndGet();
            if (newLockCount <= 0) {
                if (newLockCount < 0) {
                    throw new IllegalMonitorStateException("Lock count has gone negative for lock: " + this.basePath);
                } else {
                    //释放锁
                    try {
                        this.internals.releaseLock(lockData.lockPath);
                    } finally {
                        this.threadData.remove(currentThread);
                    }

                }
            }
        }
    }

好了,这就是我这次的分享内容,如有错请提出,谢谢。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐