一,基于 Curator 实现分布式锁
1,分布式锁的基本场景
如果在多线程并行情况下去访问某一个共享资源,比如说共享变量,那么势必会造成线程安全问题。那么我们可以用很多种方法来解决,比如 synchronized、 比如 Lock 之类的锁操作来解决线程安全问题,那么在分布式架构下,涉及到多个进程访问某一个共享资源的情况,这个时候我们需要一些互斥手段来防止彼此之间的干扰。
然后在分布式情况下,synchronized 或者 Lock 之类的锁只能控制单一进程的资源访问,在多进程架构下,这些 api就没办法解决我们的问题了。
2,用zookeeper来实现分布式锁
可以利用 zookeeper 节点的特性来实现独占锁,就是同级节点的唯一性,多个进程往 zookeeper 的指定节点下创建一个相同名称的节点,只有一个能成功,另外一个是创建失败;创建失败的节点全部通过 zookeeper 的 watcher 机制来监听 zookeeper 这个子节点的变化,一旦监听到子节点的删除事件,则再次触发所有进程去写锁;
这种实现方式很简单,但是会产生“惊群效应”,简单来说就是如果存在许多的客户端在等待获取锁,当成功获取到锁的进程释放该节点后,所有处于等待状态的客户端都会被唤醒,这个时候 zookeeper 在短时间内发送大量子节点变更事件给所有待获取锁的客户端,然后实际情况是只会有一个客户端获得锁。如果在集群规模比较大的情况下,会对 zookeeper 服务器的性能产生比较的影响。
3,利用有序节点来实现分布式锁
可以通过有序节点来实现分布式锁,每个客户端都往指定的节点下注册一个临时有序节点,越早创建的节点,节点的顺序编号就越小,那么我们可以判断子节点中最小的节点设置为获得锁。如果自己的节点不是所有子节点中最小的,意味着还没有获得锁。这个的实现和前面单节点实现的差异性在于,每个节点只需要监听比自己小的节点,当比自己小的节点删除以后,客户端会收到 watcher 事件,此时再次判断自己的节点是不是所有子节点中最小的,如果是则获得锁,否则就不断重复这个过程,这样就不会导致羊群效应,因为每个客户端只需要监控一个节点。
4,curator分布式锁的基本使用
curator 对于锁这块做了一些封装,curator 提供了InterProcessMutex 这样一个 api。除了分布式锁之外,还提供了 leader 选举、分布式队列等常用的功能。
InterProcessMutex | 分布式可重入排它锁 |
---|---|
InterProcessSemaphoreMutex | 分布式排它锁 |
InterProcessReadWriteLock | 分布式读写锁 |
public class LockDemo01 {
private static final String CONNECTION_STR = ",";
public static void main(String[] args) {
CuratorFramework curatorFramework = CuratorFrameworkFactory
.builder()
.connectString(CONNECTION_STR)
.sessionTimeoutMs(5000)
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.build();
curatorFramework.start();
InterProcessMutex lock = new InterProcessMutex(curatorFramework, "/locks");
IntStream.range(0,10).forEach(i-> new Thread(()->{
System.out.println(Thread.currentThread().getName()+"尝试获取锁!");
try {
lock.acquire();
System.out.println(Thread.currentThread().getName()+"获取锁成功!");
TimeUnit.SECONDS.sleep(4);
lock.release();
System.out.println(Thread.currentThread().getName()+"释放锁成功!");
}catch (Exception e){
}
},String.valueOf(i)).start());
}
}
5,Curator实现分布式锁的基本原理
1)变量
private final LockInternals internals;
private final String basePath;
// 记录线程与锁信息的映射关系
private final ConcurrentMap<Thread, InterProcessMutex.LockData> threadData;
private static final String LOCK_NAME = "lock-";
2)构造函数
//Zookeeper 利用 path 创建临时顺序节点,实现公平锁的核心
public InterProcessMutex(CuratorFramework client, String path) {
this(client, path, new StandardLockInternalsDriver());
}
//maxLeases=1,表示可以获得分布式锁的线程数量(跨 JVM)为 1,即为互斥锁
public InterProcessMutex(CuratorFramework client, String path, LockInternalsDriver driver) {
this(client, path, "lock-", 1, driver);
}
//internals 的 类 型 为 LockInternals ,InterProcessMutex 将分布式锁的申请和释放操作委托给internals 执行
InterProcessMutex(CuratorFramework client, String path, String lockName, int maxLeases, LockInternalsDriver driver) {
this.threadData = Maps.newConcurrentMap();
this.basePath = PathUtils.validatePath(path);
this.internals = new LockInternals(client, driver, path, lockName, maxLeases);
}
3)内部类
// 锁信息
// Zookeeper 中一个临时顺序节点对应一个“锁”,但让锁生效激活需要排队(公平锁)
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;
}
}
4)获取锁
// 无限等待
public void acquire() throws Exception {
if (!this.internalLock(-1L, (TimeUnit) null)) {
throw new IOException("Lost connection while trying to acquire lock: " + this.basePath);
}
}
// 限时等待
public boolean acquire(long time, TimeUnit unit) throws Exception {
return this.internalLock(time, unit);
}
①internalLock
private boolean internalLock(long time, TimeUnit unit) throws Exception {
Thread currentThread = Thread.currentThread();
InterProcessMutex.LockData lockData = (InterProcessMutex.LockData) this.threadData.get(currentThread);
// 实现可重入
if (lockData != null) {
// 同一线程再次 acquire,首先判断当前的映射表内(threadData)是否有该线程的锁信息,如果有 则原子+1,然后返回
lockData.lockCount.incrementAndGet();
return true;
} else {
// 映射表内没有对应的锁信息,尝试通过LockInternals 获取锁
String lockPath = this.internals.attemptLock(time, unit, this.getLockNodeBytes());
// 成功获取锁,记录信息到映射表
if (lockPath != null) {
InterProcessMutex.LockData newLockData = new InterProcessMutex.LockData(currentThread, lockPath);
this.threadData.put(currentThread, newLockData);
return true;
} else {
return false;
}
}
}
②attemptLock
// 尝试获取锁,并返回锁对应的 Zookeeper 临时顺序节点的路径
String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception {
long startMillis = System.currentTimeMillis();
// 无限等待时,millisToWait 为 null
Long millisToWait = unit != null ? unit.toMillis(time) : null;
// 创建 ZNode 节点时的数据内容,无关紧要,这里为 null,采用默认值(IP 地址)
byte[] localLockNodeBytes = this.revocable.get() != null ? new byte[0] : lockNodeBytes;
// 当前已经重试次数,与CuratorFramework的重试策略有关
int retryCount = 0;
// 在 Zookeeper 中创建的临时顺序节点的路径,相当于一把待激活的分布式锁
// 激活条件:同级目录子节点,名称排序最小 (排队,公平锁)
String ourPath = null;
// 是否已经持有分布式锁
boolean hasTheLock = false;
// 是否已经完成尝试获取分布式锁的操作
boolean isDone = false;
while (!isDone) {
isDone = true;
try {
// 从 InterProcessMutex 的构造函数可知实际 driver 为 StandardLockInternalsDriver 的实例
// 在Zookeeper中创建临时顺序节点
ourPath = this.driver.createsTheLock(this.client, this.path, localLockNodeBytes);
// 循环等待来激活分布式锁,实现锁的公平性
hasTheLock = this.internalLockLoop(startMillis, millisToWait, ourPath);
} catch (KeeperException.NoNodeException var14) {
// 因 为 会 话 过 期 等 原 因 ,StandardLockInternalsDriver 因为无法找到创建的临时 顺序节点而抛出 NoNodeException 异常
if (!this.client.getZookeeperClient().getRetryPolicy().allowRetry(retryCount++, System.currentTimeMillis() - startMillis, RetryLoop.getDefaultRetrySleeper())) {
throw var14;
}
// 满足重试策略尝试重新获取锁
isDone = false;
}
}
// 成功获得分布式锁,返回临时顺序节点的路径,上层将其封装成锁信息记录在映射表,方便锁重入
//获取分布式锁失败,返回 null
return hasTheLock ? ourPath : null;
}
③createsTheLock
// 在 Zookeeper 中创建临时顺序节点
public String createsTheLock(CuratorFramework client, String path, byte[] lockNodeBytes) throws Exception {
String ourPath;
// lockNodeBytes 不为 null 则作为数据节点内容,否则采用默认内容(IP 地址)
if (lockNodeBytes != null) {
/**
* creatingParentContainersIfNeeded:用于创建父节点,如果不支持 CreateMode.CONTAINER,
* 那么将采用 CreateMode.PERSISTENT
* withProtection:临时子节点会添加GUID前缀
* CreateMode.EPHEMERAL_SEQUENTIAL:临时顺序节点,Zookeeper 能保证在节点产生的顺序性,
* 依据顺序来激活分布式锁,从而也实现了分布式锁的公平性
*/
ourPath = (String) ((ACLBackgroundPathAndBytesable) client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL)).forPath(path, lockNodeBytes);
} else {
ourPath = (String) ((ACLBackgroundPathAndBytesable) client.create().creatingParentContainersIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL)).forPath(path);
}
return ourPath;
}
④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 {
// exists()会导致导致资源泄漏,因此 exists()可以监听不存在的 ZNode,因此采用 getData()
// 上一临时顺序节点如果被删除,会唤醒当前线程继续竞争锁,正常情况下能直接获得锁,因为锁是公平的
((BackgroundPathable) this.client.getData().usingWatcher(this.watcher)).forPath(previousSequencePath);
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 (KeeperException.NoNodeException var19) {
}
}
}
}
} catch (Exception var21) {
ThreadUtils.checkInterrupted(var21);
// 标记删除,在 finally 删除之前创建的临时顺序节点(后台不断尝试)
doDelete = true;
// 重新抛出,尝试重新获取锁
throw var21;
} finally {
if (doDelete) {
this.deleteOurPath(ourPath);
}
}
return haveTheLock;
}
⑤getsTheLock
public PredicateResults getsTheLock(CuratorFramework client, List<String> children, String sequenceNodeName, int maxLeases) throws Exception {
// 之前创建的临时顺序节点在排序后的子节点列表中的索引
int ourIndex = children.indexOf(sequenceNodeName);
// 校验之前创建的临时顺序节点是否有效
validateOurIndex(sequenceNodeName, ourIndex);
/**
* 锁公平性的核心逻辑
* 由 InterProcessMutex 的构造函数可知,
* maxLeases 为 1,即只有 ourIndex 为 0 时,线程才能持有锁,
* 或者说该线程创建的临时顺序节点激活了锁.
* Zookeeper 的临时顺序节点特性能保证跨多个 JVM 的线程并发创建节点时的顺序性,
* 越早创建临时顺序节点成功的线程会更早地激活锁或获得锁
*/
boolean getsTheLock = ourIndex < maxLeases;
/**
* 如果已经获得了锁,则无需监听任何节点,否则需要监听上一顺序节点(ourIndex-1)
* 因为锁是公平的,因此无需监听除了(ourIndex-1)以外的所有节点,这是为了减少羊群效应.
*/
String pathToWatch = getsTheLock ? null : (String)children.get(ourIndex - maxLeases);
//返回获取锁的结果,交由上层继续处理 添加监听等操作
return new PredicateResults(pathToWatch, getsTheLock);
}
5)释放锁
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();
// 锁是可重入的,初始值为 1,原子-1 到 0,锁才释放
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);
}
}
}
}
}
①releaseLock
final void releaseLock(String lockPath) throws Exception {
//移除监听者
this.client.removeWatchers();
this.revocable.set((Object)null);
//删除临时顺序节点,只会触发后一顺序节点去获取锁,
//理论上不存在竞争,只排队,非抢占,公平锁,先到先得
this.deleteOurPath(lockPath);
}
②deleteOurPath
private void deleteOurPath(String ourPath) throws Exception {
try {
// 后台不断尝试删除
((ChildrenDeletable)this.client.delete().guaranteed()).forPath(ourPath);
} catch (KeeperException.NoNodeException var3) {
}
}