02_zookeeper分布式锁源码分析
一. 可重入的公平锁
1.1 基本的使用方式
private static CuratorFramework getConnection() {
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.newClient(
"xxx.xxx.xxx.xxx:2181,xxx.xxx.xxx.xxx:2181",
retryPolicy);
client.start();
return client;
}
private static void createMutexTest(CuratorFramework client, String lockPath) throws Exception {
InterProcessMutex lock = new InterProcessMutex(client, lockPath);
lock.acquire();
TimeUnit.SECONDS.sleep(3);
lock.release();
}
public static void main(String[] args) throws Exception {
// 启动zookeeper客户端,获取一个zookeeper连接
CuratorFramework client = getConnection();
// 测试可重入锁
createMutexTest(client, "/locks/lock_01");
}
1.2 初次加锁
1.InterProcessMutex acquire( )
public void acquire() throws Exception
{
if ( !internalLock(-1, null) )
{
throw new IOException("Lost connection while trying to acquire lock: " + basePath);
}
}
调用internalLock( )时,传入的time=-1,Unit=null。
2.InterProcessMutex internalLock( )
private boolean internalLock(long time, TimeUnit unit) throws Exception
{
/*
Note on concurrency: a given lockData instance
can be only acted on by a single thread so locking isn't necessary
*/
Thread currentThread = Thread.currentThread();
LockData lockData = threadData.get(currentThread);
if ( lockData != null )
{
// re-entering
lockData.lockCount.incrementAndGet();
return true;
}
String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
if ( lockPath != null )
{
LockData newLockData = new LockData(currentThread, lockPath);
threadData.put(currentThread, newLockData);
return true;
}
return false;
}
通过源码上的注释,我们得知,虽然InterProcessMutex可能会被不同的线程使用,但是呢,每个线程都会有对应的lockData,lockData是线程独享的。
这里的threadData和lockData都非常重要,当前线程是否持有锁,可重入加锁的实现逻辑等等都与它息息相关,所以,我们来看看threadData的源码吧。
private final ConcurrentMap<Thread, LockData> threadData = Maps.newConcurrentMap();
private static class LockData
{
final Thread owningThread;
final String lockPath;
final AtomicInteger lockCount = new AtomicInteger(1);
private LockData(Thread owningThread, String lockPath)
{
this.owningThread = owningThread;
this.lockPath = lockPath;
}
}
threadData是一个支持并发的Map结构,Key是线程对象,Value是LockData,也就是该线程持有锁的相关信息。
LockData由线程对象、锁的路径、加锁的次数,这三个变量构成,第一次初始化后,加锁的次数等于1。
回到internalLock( )内,初次加锁时,threadData必然是空的,更别提lockData了。此时就会继续往下执行,进入LockInternals的attemptLock( )。
String ourPath = client.create()
.creatingParentsIfNeeded().withProtection()
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path);
creatingParentsIfNeeded() :如果父节点不存在,则创建父节点。
withMode(CreateMode.EPHEMERAL_SEQUENTIAL): 创建节点的方式是临时顺序节点。
path: /locks/lock_01/lock-
最终计算出的ourPath=/locks/lock_01/_c_aa0701c8-a84a-4c1e-9590-0b61b20f7db8-lock-0000000001
hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath);
3.LockInternals internalLockLoop( )
查看当前线程是否持有这把锁。
获取到父目录下的所有子节点,每个子节点代表着一个线程发起获取锁的请求,然后呢,会对子节点进行排序,默认是升序排序,也就是说,先申请锁的节点会被排在队头,后申请锁的节点会被排到队尾。
4.StandardLockInternalsDriver getsTheLock( ) 核心方法
属于第三步的核心方法,用于校验当前线程是否能成功的获取锁。具体的代码如下:
public PredicateResults getsTheLock(CuratorFramework client, List<String> children, String sequenceNodeName, int maxLeases) throws Exception
{
int ourIndex = children.indexOf(sequenceNodeName);
validateOurIndex(sequenceNodeName, ourIndex);
boolean getsTheLock = ourIndex < maxLeases;
String pathToWatch = getsTheLock ? null : children.get(ourIndex - maxLeases);
return new PredicateResults(pathToWatch, getsTheLock);
}
查看本次的节点处于所有子节点的哪个位置,由于是初次获取锁,此时outIndex必然等于0。
看到boolean getsTheLock = ourIndex < maxLeases,由于maxLeases等于1,getsTheLock=0<1=true。这就表明,当前线程成功获取锁。
5.LockInternals internalLockLoop( )的剩余代码
回到InterProcessMutex的internalLock( )方法,此时我们已经获取到了锁,并且知道了锁对应节点的路径。所以会根据当前线程和锁路径创建出LockData,最后放到threadData中。
1.3 同客户端、同线程,重入加锁的过程
相同客户端、相同线程进行加锁时,请求会落入InterProcessMutex的internalLock( )。此时,threadData有值,并且lockData也不为空。所以会执行以下语句,将加锁次数加1,然后就执行返回了,锁获取成功,就这么简单。
注意: Curator框架重入加锁时,不会对zookeeper发起任何请求,这一点与Redis不同。
lockData.lockCount.incrementAndGet();
1.4 加锁时的锁互斥
相同的客户端,不同的线程,或者不同的客户端尝试获取一把已经被其他人持有的锁时,又会发生什么呢?
我们还是回到InterProcessMutex的internalLock( ),由于当前客户端尚未获得这把锁,所以lockData一定为空。
接着就开始尝试获取锁了。
String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
就像1.1节中写的那样,获取锁的过程无非就是查询锁目录下的所有节点,组成一个list,查看当前节点是不是list中的头节点,由于已经有人持有锁,所以当前节点肯定不是list的下标为0的节点。
此时,我们拿到当前节点的前一个节点的绝对路径,对其做一个监听器,然后就用Object wait( )陷入无限的等待。
String previousSequencePath = basePath + "/" + predicateResults.getPathToWatch();
synchronized(this) {
Stat stat = client.checkExists().usingWatcher(watcher).forPath(previousSequencePath);
... 代码省略
}
过了一段时间后,这把锁被释放,对应在zookeeper的节点也会被回收,此时zookeeper会通知我们之前配置好的监听器,由监听器调用Object notifyAll( ),唤醒所有处于等待中的线程。于是,我们进入下一轮的循环,继续尝试获取锁。之后重复上述过程,直到我们前面没有其它节点,获取锁成功。
@Override
private final Watcher watcher = new Watcher()
{
@Override
public void process(WatchedEvent event)
{
notifyFromWatcher();
}
};
从这里也不难看出,curator底层实现zookeeper分布式锁时使用了的数据结构是顺序节点,严格的控制了客户端获取锁的顺序。想要获得锁,就必须等待比你之前获取锁的客户端释放锁,否则只能老老实实的处于wait状态。
1.5 释放锁
既然是释放锁,那么代码转到InterProcessMutex的release( )。
首先,判断待释放锁的客户端是不是目前持有这把锁的线程,如果不是,则直接抛出异常。
接着,减小当前线程对于这把锁的加锁次数。
最后,观察锁的加锁次数,如果加锁次数大于0,说明当前线程对这把锁进行了多次请求,而每次加锁都需要对应一次解锁,这里还有剩余的加锁次数需要扣减,所以直接返回,表示解锁成功。如果加锁次数正等于0,说明本次解锁,刚好对应上了加锁的次数,当前线程已经不再需要这把锁,此时会调用LockInternals的releaseLock( )方法释放这把锁,底层其实就是去删除对应路径的节点(就是/leases目录下对应的临时顺序节点)。
@Override
public void release() throws Exception
{
Thread currentThread = Thread.currentThread();
LockData lockData = threadData.get(currentThread);
if ( lockData == null )
{
throw new IllegalMonitorStateException("You do not own the lock: " + basePath);
}
int newLockCount = lockData.lockCount.decrementAndGet();
if ( newLockCount > 0 )
{
return;
}
if ( newLockCount < 0 )
{
throw new IllegalMonitorStateException("Lock count has gone negative for lock: " + basePath);
}
try
{
internals.releaseLock(lockData.lockPath);
}
finally
{
threadData.remove(currentThread);
}
}
1.6 同一个InterProcessMutex对象是否支持多个线程使用?
答: 支持。在InterProcessMutex的源码中,我们知道threadData是一个支持并发的ConcurrentMap数据结构,会为每个线程维护独立的LockData对象,存放了对应线程的锁信息。既然的锁信息是独立的,那么多个线程使用同一个InterProcessMutex当然也是可以的。
二. Semaphore信号量
2.1 基本的使用方式
private static void semaphoreTest(CuratorFramework client) throws Exception {
int maxLease = 3;
String path = "/semaphore/semaphore_01";
InterProcessSemaphoreV2 semaphore = new InterProcessSemaphoreV2(client, path, maxLease);
Lease lease = semaphore.acquire();
TimeUnit.SECONDS.sleep(3);
semaphore.returnLease(lease);
}
maxLease: 信号量的个数。同时允许获取信号量的最大线程数量。
上述代码中,同时只允许最多3个线程获取信号量,其它的线程想要获取信号量,必须等到其它线程归还信号量才可以。
2.2 获取信号量
假设有这样一个场景:若某个线程希望获取锁,且这个信号量还有空闲的锁。
1.获取第一把锁 /locks
获取第一把锁“/locks”,它的路径为: /semaphore/semaphore_01/locks。在获取信号量之前,必须保证能获取这把锁。
这是一把可重入的、公平的、互斥锁。若已经有其他人获取到这把锁了,则当前线程会陷入等待,直到获取到这把锁。
2.获取第二把锁 /leases
只要能获取到/locks这把锁,那么当前线程就需要向xxx/leases目录下创建临时顺序节点。
接着,查看xxx/leases目录下的节点个数,若节点个数 <= 信号量的个数,那么获取信号量成功。若不满足,则当前线程会陷入等待状态,并监听xxx/leases目录下的所有节点,只要有任何一个节点被删除,说明有人归还了信号量,此时线程就会被唤醒,并再一次查询xxx/leases目录下节点的个数,之后不断地循环上述过程,直到能成功获得信号量为止。
可能有人觉得奇怪,为什么要先添加临时顺序节点,然后再检查目录下节点的个数?难道就不怕在当前线程的等待过程中,其它的线程过来尝试添加临时顺序节点吗?不用担心,因为其它线程根本就不可能执行到添加临时顺序节点的代码位置,还记得第一把锁吗,此时当前线程处于wait状态,尚未释放第一把锁,因此其它线程都会阻塞在获取第一把锁的地方!
3. 释放第一把锁 /locks
执行InterProcessMutex release()释放锁。
总结一下,Curator对于semaphore的实现,无非就是搞了两把锁。
第一把锁的作用:控制所有的客户端严格按照顺序来获取信号量,保证并发下的顺序性。
第二把锁的作用:保证只有有限个信号量可以供客户端获取。信号量的个数是通过限制指定目录下节点的个数来保证的,这一点比较巧妙。
三. 不可重入的公平锁
3.1 基本的使用方式
InterProcessSemaphoreMutex semaphoreMutex = new InterProcessSemaphoreMutex(client, "/mutex/semaphore_01");
semaphoreMutex.acquire();
semaphoreMutex.acquire();
semaphoreMutex.release();
3.2 初次加锁
看到InterProcessSemaphoreMutex,不可重入的公平锁,获取锁的逻辑底层使用的就是Semaphore,只不过调用了Semaphore的acquire()无参方法,信号量的个数等于1。也就是说,同时只能有一个线程获得信号量。
private final InterProcessSemaphoreV2 semaphore;
public void acquire() throws Exception
{
lease = semaphore.acquire();
}
3.2 再次加锁(重入锁)
这里完全就是在重放一遍Semaphore的逻辑。假设线程A已经获取到了这把不可重入的公平锁了。此时又来了线程B尝试获取锁。让我们来看看会发生什么。
首先,线程B尝试获取第一把锁,假设此时能获取到。
接着,针对第二把锁,添加临时顺序节点,添加成功后,判断第二把锁目录下的节点的个数是否小于等于信号量的个数。由于2 <=1 不成立,因此当前线程陷入无线等待(wait),并且监听第二把锁下目录内的所有节点。
过了一段时间,线程A释放了不可重入的公平锁,那么针对第二把锁(/leases),就会删除它自己对应的那个临时顺序节点。此时,线程B的监听器就会感知到zookeeper上发生的这一系列变故,接着就会执行notifyAll()唤醒被这个锁对象wait的线程,这就包括了线程B。
四. 可重入的读写锁
源码的分析过程看一遍,脑子里有大概的印象,之后直接看结论就好了。
4.1 基本的使用方式
InterProcessReadWriteLock readWriteLock = new InterProcessReadWriteLock(client, "/read_write_lock/lock01");
InterProcessMutex readLock = readWriteLock.readLock();
readLock.acquire();
readLock.release();
InterProcessMutex writeLock = readWriteLock.writeLock();
writeLock.acquire();
writeLock.release();
4.2 同客户端同线程,先读锁,后写锁
结论: 同一个客户端,同线程,先读锁,后写锁,是互斥的。
获取读锁且没有释放的前提下,再次获取写锁时,当前线程会被阻塞住,直到读锁被释放。
通过观察InterProcessReadWriteLock的内部结构,我们发现,它就是搞了两把可重入的公平锁InterProcessMutex。
public class InterProcessReadWriteLock {
private final InterProcessMutex readMutex;
private final InterProcessMutex writeMutex;
private static final String READ_LOCK_NAME = "__READ__";
private static final String WRITE_LOCK_NAME = "__WRIT__";
... 省略代码
}
需要注意的是,InterProcessReadWriteLock重写了判断是否成功获取锁的代码逻辑,也就是LockInternalsDriver的getsTheLock( )方法。
针对加读锁而言,判断能否成功获取锁的代码如下:
public PredicateResults getsTheLock(CuratorFramework client, List<String> children,
String sequenceNodeName, int maxLeases) throws Exception
{
return readLockPredicate(children, sequenceNodeName);
}
private PredicateResults readLockPredicate(List<String> children, String sequenceNodeName) throws Exception
{
if ( writeMutex.isOwnedByCurrentThread() )
{
return new PredicateResults(null, true);
}
int index = 0;
int firstWriteIndex = Integer.MAX_VALUE;
int ourIndex = Integer.MAX_VALUE;
for ( String node : children )
{
if ( node.contains(WRITE_LOCK_NAME) )
{
firstWriteIndex = Math.min(index, firstWriteIndex);
}
else if ( node.startsWith(sequenceNodeName) )
{
ourIndex = index;
break;
}
++index;
}
StandardLockInternalsDriver.validateOurIndex(sequenceNodeName, ourIndex);
boolean getsTheLock = (ourIndex < firstWriteIndex);
String pathToWatch = getsTheLock ? null : children.get(firstWriteIndex);
return new PredicateResults(pathToWatch, getsTheLock);
}
首先呢,为了获取读锁,当前线程一定会在指定目录下创建一个临时顺序节点。我们的锁目录是/read_write_lock/lock01,
此时锁目录的节点情况如下:
/read_write_lock/lock01/_c_acf47c62-d237-4f70-bbf9-f57a3141bac1-__READ__0000000000
接着,就会检查当前线程是否持有写锁,如果持有,则直接返回,加读锁成功。但是我们现在是在加读锁啊,所以肯定是不成立的。
继续往下看,curator拿到了锁目录下的所有临时顺序节点,检查当前线程是否曾经尝试获取过写锁,此时不成立,所以会遍历所有的临时顺序节点,找到本次请求创建出的节点,下标是0,然后就会终止循环。
重点来了,这里会去判断,本次请求获取读锁时,创建的临时节点的下标是否小于Integer.MAX_VALUE。这不是废话吗?肯定小于啊,所以标记为获取读锁成功,pathToWatch=null,意味着不会去监听任何的路径或者节点(我都已经成功获取锁了,干嘛还要监听别的节点呢?)。
现在读锁已经获取完毕了,我们开始获取写锁。
首先呢,写锁同样是一个InterProcessMutex,为了获得写锁,这里会创建的临时顺序节点。
此时锁目录的节点情况如下:
_c_acf47c62-d237-4f70-bbf9-f57a3141bac1-__READ__0000000000
_c_d087af7e-bb22-4af3-92b2-cf0317b7a7de-__WRIT__0000000001
然后就是判断本次请求获取写锁是否成功啊?这儿又会走getsTheLock( ),与读锁不同的是,InterProcessReadWriteLock没有覆盖写锁的getsTheLock( ),使用的默认的代码逻辑。这个逻辑我们熟悉的很呐,想要获得锁,当前请求添加的节点必须位于节点列表的第一个位置,但遗憾的是,现在的index=1,不等于0啊,所以这里就会阻塞住,并且搞了一个监听器,监听前一个节点,也就是读锁,当且仅当读锁被释放掉,再尝试获取写锁,最后才能获取写锁成功。
4.3 同客户端同线程,先写锁,后读锁
结论: 同一个客户端,同线程,先写锁,后读锁,可以加锁成功。
首先,加一把写锁,此时锁目录的节点情况如下:
_c_d087af7e-bb22-4af3-92b2-cf0317b7a7de-__WRIT__0000000001
接着,尝试加一把读锁,这里又会向锁目录创建一个临时顺序节点,此时锁目录的节点情况如下:
_c_d087af7e-bb22-4af3-92b2-cf0317b7a7de-__WRIT__0000000001
_c_acf47c62-d237-4f70-bbf9-f57a3141bac1-__READ__0000000000
然后就是判断是否加读锁成功的逻辑了,之前我们看到过,InterProcessReadWriteLock重写了读锁的getsTheLock(),这个方法内有一个非常重要的代码:
private PredicateResults readLockPredicate(List<String> children, String sequenceNodeName) throws Exception
{
if ( writeMutex.isOwnedByCurrentThread() )
{
return new PredicateResults(null, true);
}
后面的代码都不用看了...
}
写锁就是由当前线程持有的,所以直接返回true,获取读锁成功。
4.4 同客户端同线程,先写锁,后重入写锁
结论: 同一个客户端,同线程,先写锁,后重入写锁,可以加锁成功。
你先加一个写锁,接着后面加无数个写锁都没问题,无非就是累加lockData中,加锁的次数而已。
4.5 同客户端同线程,先读锁,后重入读锁
结论: 同一个客户端,同线程,先读锁,后重入读锁,可以加锁成功。
你先加一个读锁,接着后面加无数个读锁都没问题,无非就是累加lockData中,加锁的次数而已。
4.6 不同客户端,或相同客户端不同线程,先读锁,后写锁
结论: 不同客户端,或者相同客户端的不同线程,先读锁,后写锁,是互斥的!
首先,客户端A加一把读锁,此时锁目录的情况如下:
_c_acf47c62-d237-4f70-bbf9-f57a3141bac1-__READ__0000000000
接着,客户端B,或者客户端A的另一个线程过来加一把写锁,这里一上来又会添加一个临时顺序节点,此时锁目录的情况如下:
_c_acf47c62-d237-4f70-bbf9-f57a3141bac1-__READ__0000000000
_c_d087af7e-bb22-4af3-92b2-cf0317b7a7de-__WRIT__0000000001
看到写锁的节点前面有其他的节点,那想都不用想,肯定就是互斥的,后面就是阻塞啊,监听器啊,等待前面的节点释放锁啊之类的过程。具体可以参考4.2节。
4.7 不同客户端,或相同客户端不同线程,先写锁,后读锁
结论: 不同客户端,或者相同客户端的不同线程,先写锁,后读锁,是互斥的!
首先,加一把写锁,此时锁目录的节点情况如下:
_c_d087af7e-bb22-4af3-92b2-cf0317b7a7de-__WRIT__0000000001
接着,尝试加一把读锁,这里又会向锁目录创建一个临时顺序节点,此时锁目录的节点情况如下:
_c_d087af7e-bb22-4af3-92b2-cf0317b7a7de-__WRIT__0000000001
_c_acf47c62-d237-4f70-bbf9-f57a3141bac1-__READ__0000000000
然后就是判断是否加读锁成功的逻辑了,之前我们看到过,InterProcessReadWriteLock重写了读锁的getsTheLock(),这个方法内有一个非常重要的代码:
private PredicateResults readLockPredicate(List<String> children, String sequenceNodeName) throws Exception
{
if ( writeMutex.isOwnedByCurrentThread() )
{
return new PredicateResults(null, true);
}
int index = 0;
int firstWriteIndex = Integer.MAX_VALUE;
int ourIndex = Integer.MAX_VALUE;
for ( String node : children )
{
if ( node.contains(WRITE_LOCK_NAME) )
{
firstWriteIndex = Math.min(index, firstWriteIndex);
}
else if ( node.startsWith(sequenceNodeName) )
{
ourIndex = index;
break;
}
++index;
}
StandardLockInternalsDriver.validateOurIndex(sequenceNodeName, ourIndex);
boolean getsTheLock = (ourIndex < firstWriteIndex);
String pathToWatch = getsTheLock ? null : children.get(firstWriteIndex);
return new PredicateResults(pathToWatch, getsTheLock);
}
看到for循环,首先当前锁目录下存在一个包含了"__WRIT__"的节点,换句话说,就是之前加过写锁,所以执行这句话:
firstWriteIndex = Math.min(index, firstWriteIndex);
index指的是当前循环到的这个元素的下标,firstWriteIndex等于Interger.MAX_VALUE,这里取较小的元素,必然就是index,此时等于0。
boolean getsTheLock = (ourIndex < firstWriteIndex);
ourIndex等于本次请求添加的临时顺序节点的下标,此时等于1。
boolean getsTheLock = (1 < 0) = false 获取读锁失败。
也就是说,只要在你之前有人加了写锁,你再去加读锁时,是加不了的,是互斥的。
4.8 不同客户端,或相同客户端不同线程,先写锁,后写锁
结论: 不同客户端,或者相同客户端的不同线程,先写锁,后写锁,是互斥的!
参考4.6节,反正写锁对应的节点如果没有位于临时顺序节点的第一个位置,那肯定就是没办法加上锁的。
4.9 不同客户端,或相同客户端不同线程,先读锁,后读锁
结论: 不同客户端,或者相同客户端的不同线程,先读锁,后读锁,可以加锁成功。
读锁检查加锁是否成功的逻辑被InterProcessReadWriteLock重写了,重写了,重写了。 这句话我都要写吐了。。。
只要本次加读锁对应的临时顺序节点在列表中的下标不超过Integer.MAX_VALUE,那就可以成功加锁。
Integer.MAX_VALUE可是2147483647,这么大的数值,显然无数个客户端同时加读锁,都是没问题的。
五. MultiLock
5.1 基本的使用方式
InterProcessLock lock01 = new InterProcessMutex(client, "/multi_lock/lock01");
InterProcessLock lock02 = new InterProcessMutex(client, "/multi_lock/lock02");
InterProcessLock lock03 = new InterProcessMutex(client, "/multi_lock/lock03");
List<InterProcessLock> lockList = new ArrayList<>(4);
lockList.add(lock01);
lockList.add(lock02);
lockList.add(lock03);
InterProcessMultiLock multiLock = new InterProcessMultiLock(lockList);
multiLock.acquire();
multiLock.release();
5.2 加锁的过程
MultiLock的内部就是搞了一个集合,存放了多把锁。
private final List<InterProcessLock> locks;
获取锁时,就是在循环集合内的每一把锁,并尝试加锁,把加成功的锁放到放到一个集合里面。
只要任何一把锁加锁失败,就会依次释放之前成功加到的锁,最后返回false。
当且仅当所有的锁全部加锁成功,则返回true。
5.3 解锁的过程
循环遍历集合内的每一把锁,接着就是去解锁。
public synchronized void release() throws Exception
{
Exception baseException = null;
for ( InterProcessLock lock : reverse(locks) )
{
try
{
lock.release();
}
catch ( Exception e )
{
if ( baseException == null )
{
baseException = e;
}
else
{
baseException = new Exception(baseException);
}
}
}
if ( baseException != null )
{
throw baseException;
}
}
总感觉这坨代码有bug,如果释放前一把锁失败,接着释放下一把锁也释放,那么后面捕获的异常岂不是把前者给覆盖掉了?那么抛出的异常就是按照顺序的,最后一个解锁失败的锁对应的异常了。
也许这就是curator的想要实现的逻辑吧…