前言
对于分布式锁的实现,我们一般需要考虑如下:
- 保证分布式锁的互斥性。同一时刻,只有一个客户端成功获取到锁。
- 多个客户端同时请求按照什么样的顺序来获取锁。
- 没有获取到锁的客户端如何重新尝试获取锁。
- 如何保证只有成功获取到锁的客户端才能释放锁。
- 如何保证获取锁的客户端成功释放锁,避免死锁问题。
本文基于 Curator 5.4.0 版本,展开对分布式锁的分析。
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.4.0</version>
</dependency>
分布式可重入锁
即 InterProcessMutex。
可重入:一个持有锁的线程在没有释放锁的情况下,可以再次获取这个锁。
具体地,每个客户端在指定路径下创建一个临时顺序节点。选取序号最小的节点进行抢占锁。然后对这个节点路径添加一个监听器,抢占失败的节点进入等待状态(或者超时等待状态)一旦持有锁的节点释放锁,这些等待的节点会被唤醒,然后尝试抢占锁。
Curator 使用 InterProcessMutex 的 acquire、release 方法表达抢占锁、释放锁。使用 ConcurrentHashMap 存储线程与 LockData 之间的对应关系,如果 LockData 为空,表示当前没有线程持有锁;反之,表示当前线程持有锁。使用 LockData#lockCount 属性记录持有锁的次数。
使用
public class CuratorDemo {
private static final Logger LOGGER = LoggerFactory.getLogger(CuratorDemo.class);
private static final String CONNECT_STRING = "127.0.0.1:2181";
private static RetryPolicy initRetryPolicy() {
return new ExponentialBackoffRetry(500, 5, 5000);
}
private static CuratorFramework initCuratorFramework() {
CuratorFramework curatorFramework = CuratorFrameworkFactory
.builder()
.connectString(CONNECT_STRING)
.retryPolicy(initRetryPolicy())
.build();
curatorFramework.start();
return curatorFramework;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
CuratorFramework curatorFramework = initCuratorFramework();
InterProcessMutex interProcessMutex = new InterProcessMutex(curatorFramework, "/mutex");
try {
interProcessMutex.acquire();
LOGGER.info(Thread.currentThread().getName() + " acquired the lock");
Thread.sleep(2000);
} catch (Exception e) {
LOGGER.error(Thread.currentThread().getName() + " encountered error");
} finally {
try {
interProcessMutex.release();
} catch (Exception e) {
LOGGER.error(Thread.currentThread().getName() + " failed to release the lock");
}
}
}).start();
}
}
}
源码分析
从上述的例子分析,实际的调用的结构如下:
InterProcessMutex mutex = new InterProcessMutex(curatorFramework, path);
try {
mutex.acquire();
// 业务处理
} finally {
mutex.release();
}
接下来先看下 InterProcessMutex 的构造器方法。
public InterProcessMutex(CuratorFramework client, String path)
{
// 指定 LockInternalsDriver 为 StandardLockInternalsDriver 实例
this(client, path, new StandardLockInternalsDriver());
}
public InterProcessMutex(CuratorFramework client, String path, LockInternalsDriver driver)
{
// 指定 lockName 为 "lock-",maxLeases 为 1
this(client, path, LOCK_NAME, 1, driver);
}
InterProcessMutex(CuratorFramework client, String path, String lockName, int maxLeases, LockInternalsDriver driver)
{
// 校验节点路径
basePath = PathUtils.validatePath(path);
// 构建 LockInternals 实例
internals = new LockInternals(client, driver, path, lockName, maxLeases);
}
然后重点看下它的 acquire 方法。它有两种方法实现 - acquire() 阻塞直到成功获取到锁、acquire(long time, TimeUnit unit) 阻塞直到成功获取到锁或者超时。
@Override
public void acquire() throws Exception
{
if ( !internalLock(-1, null) )
{
throw new IOException("Lost connection while trying to acquire lock: " + basePath);
}
}
@Override
public boolean acquire(long time, TimeUnit unit) throws Exception
{
return internalLock(time, unit);
}
可以看出 acquire 的两种重载方法内部都会调用 internalLock 方法。
private boolean internalLock(long time, TimeUnit unit) throws Exception
{
Thread currentThread = Thread.currentThread();
// 从 threadData 缓存中获取指定线程对应的 LockData 实例
LockData lockData = threadData.get(currentThread);
// 如果 LockData 实例不为空,即表示当前线程已持有锁
if ( lockData != null )
{
// 对持有锁的次数加一
lockData.lockCount.incrementAndGet();
// 返回 true,表示获取锁成功
return true;
}
// 抢占锁
String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
// 如果抢占成功
if ( lockPath != null )
{
// 构造 LockData 实例
LockData newLockData = new LockData(currentThread, lockPath);
// 将当前线程、LockData 实例添加到 threadData 缓存中
threadData.put(currentThread, newLockData);
// 返回 true,表示获取锁成功
return true;
}
// 返回 false,表示获取锁失败
return false;
}
接着分析如何抢占锁的。
String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception
{
final long startMillis = System.currentTimeMillis();
final Long millisToWait = (unit != null) ? unit.toMillis(time) : null;
final byte[] localLockNodeBytes = (revocable.get() != null) ? new byte[0] : lockNodeBytes;
int retryCount = 0;<