因为Zookerper中的节点是不可重复创建的,因此,可以起到和redis中的setnx一样的作用,在分布式环境下,充当分布式锁来控制资源的并发处理。
bin/zkCli.sh -server 192.168.0.108:2181,192.168.0.108:2182,192.168.0.108:2183,192.168.0.108:2184
客户端1创建节点/lock
客户端2创建节点/lock
会因为已存在无法创建
获取锁流程
如上操作虽然可以实现分布式锁,但只适用于并发比较小的场景下;若是并发数量很大,会同时导致多个客户端线程监听加锁的节点,当锁被释放(节点被删除)的时候,要通知到所有获取失败且在监听的客户端,并再次触发竞争,这就是羊群效应,而且,因为没有有序的队列作为排序,这种竞争是非公平的。
因此,为了解决该问题,可以使用临时有序节点进行分布式锁的公平控制。
其实现步骤:
1.客户端建立连接后,创建临时有序节点
2.判断自己创建的节点是否是父节点下最小序号节点,是则获取锁;否则监听上一序号节点
3.执行对应的业务逻辑,释放锁,通知下一节点获取锁,重复步骤2。
中断节点
假设线程1先获取锁执行业务逻辑,还未释放锁的时候,节点2客户端断开连接,此时也会通知节点3的客户端进行步骤2判断,此时若节点1还存在,则它最小,节点3的客户端监听节点1。
幽灵节点
客户端创建节点成功,但是服务端响应失败,导致客户端不知道之前的创建结果,会进行重连然后创新创建,这样会导致实际最先创建的子节点一直存在。
解决方式:
节点创建时会带上自己客户端的唯一标识,并将创建的子节点缓存在本地,在重连后会先判断是否存在该标识的子节点,若存在则不重复创建。
如上借助于临时顺序节点,可以避免同时多个节点的并发竞争锁,缓解了服务端压力。这种实现方式所有加锁请求都进行排队加锁,是公平锁的具体实现。
使用Zookerper建立分布式锁,利用使用curtor建立
初始化
@Bean(initMethod = "start")
public CuratorFramework curatorFramework(){
//每隔1s重试,最多重试3次
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.newClient("192.168.0.108:2181", retryPolicy);
return client;
}
加锁
@PostMapping("/stock/deduct")
public Object reduceStock(Integer id) throws Exception {
InterProcessMutex interProcessMutex = new InterProcessMutex(curatorFramework, "/product_" + id);
try {
// 加锁
interProcessMutex.acquire();
orderService.reduceStock(id);
} catch (Exception e) {
if (e instanceof RuntimeException) {
throw e;
}
}finally {
//释放锁 避免死锁
interProcessMutex.release();
}
return "ok:" + port;
}
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) {
//锁重入
lockData.lockCount.incrementAndGet();
return true;
} else {
//尝试加锁
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;
}
}
}
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 {
ourPath = this.driver.createsTheLock(this.client, this.path, localLockNodeBytes);
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;
}
容器节点的方式进行创建,其特性在于若容器节点中无字节的,会被清理掉
public String createsTheLock(CuratorFramework client, String path, byte[] lockNodeBytes) throws Exception {
String ourPath;
if (lockNodeBytes != null) {
//创建容器节点creatingParentContainersIfNeeded
//临时顺序字节点EPHEMERAL_SEQUENTIAL
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;
}
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 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);
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 static List getSortedChildren(CuratorFramework client, String basePath, final String lockName, final LockInternalsSorter sorter) throws Exception {
try {
List children = (List)client.getChildren().forPath(basePath);
List sortedList = Lists.newArrayList(children);
Collections.sort(sortedList, new Comparator() {
public int compare(String lhs, String rhs) {
return sorter.fixForSorting(lhs, lockName).compareTo(sorter.fixForSorting(rhs, lockName));
}
});
return sortedList;
} catch (NoNodeException var6) {
return Collections.emptyList();
}
}
当锁释放后会唤醒等待的客户端
default CompletableFuture postSafeNotify(Object monitorHolder) {
return this.runSafe(() -> {
synchronized(monitorHolder) {
monitorHolder.notifyAll();
}
});
}
不管是使用单节点的分布式锁,还是使用子节点的临时顺序节点加锁,锁的特性都是互斥的,即同一时间只能有一个请求获取到锁进行处理,若是在大并发的场景下,性能是会急剧下降的,因此,针对这类情况,可以使用共享锁来处理。
问题类型
但由于实际业务复杂性,单纯的共享锁也存在一些问题。
双写不一致
上图,线程1在写数据库后,还未更新到缓存中时;此时线程2有对同一个值有了更新,且比线程1先更新到缓存中;在线程2更新缓存后,线程1又去更新缓存,会导致预期缓存中的结果应该是线程2的更新值,实际变为线程1的更新值,与数据库中最新的值(线程2的更新值)不一致。
读写并发不一致
另一种仅仅是在首次查询后去更新缓存的处理,也存在问题。
上图,线程1、2先后更新了数据库并删除了缓存,但线程3在线程2写数据库之前读取了线程1的数据库结果,并在线程2删除缓存后更新了缓存中值;导致预期缓存值应该是8结果更新为10。
解决方式
共享锁的监听机制,和公平锁类似,区别在于,同是读锁,线程2和1都能获取锁,但写锁线程3需要等待1和2释放才能获取到,先监听节点2,若其释放,再遍历子节点,看线程1是否释放,若是,则获取锁,否则等待;而线程4需要监听等待节点3释放才可以获取锁;同为写请求的节点7只需要监听节点6即可,机制和公平锁一样。