前言
搬起电脑,拿起键盘,回想上一次写文章,好像已经想不起来啥时候写的了。
并不是因为偷懒啊,因为在准备面试,而后又进行了一波找工作之旅,也算是收获还不错,面了6家收到了5个offer,然后敲定了工作。不过还没有去面各种大厂,因为感觉目前的技术还达不到大厂的要求,而且算法这块也比较烂。但这次也算是找到了一个挺不错的公司,进大厂就当做是下一阶段的小目标吧。
回到正题,来看分布式锁,在上一篇中也已经介绍了何为分布式锁,也说了基于redis实现的分布式锁,这一篇就来介绍基于zookeeper来实现分布式锁。
zookeeper实现
很多小伙伴都知道在分布式系统中,可以用zk来做注册中心,但其实在除了做祖册中心以外,用zk来做分布式锁也是很常见的一种方案。
先来看一下zk中是如何创建一个节点的
zk中存在create [-s] [-e] path [data]
命令,-s
为创建有序节点,-e
创建临时节点。
这样就创建了一个父节点并为父节点创建了一个子节点,组合命令意为创建一个临时的有序节点。而zk中分布式锁主要就是靠创建临时的顺序节点来实现的。至于为什么要用顺序节点和为什么用临时节点不用持久节点?先考虑一下,下文将作出说明。
同时还有zk中如何查看节点
zk中ls [-w] path
为查看节点命令,-w
为添加一个watch(监视器),/
为查看根节点所有节点,可以看到我们刚才所创建的节点,同时如果是跟着指定节点名字的话为查看指定节点下的子节点。
后面的00000000为zk为顺序节点增加的顺序。注册监听器也是zk实现分布式锁中比较重要的一个东西,下面来看一下zk实现分布式锁的主要流程。
1、当第一个线程进来时会去父节点上创建一个临时的顺序节点
2、第二个线程进来发现锁已经被持有了,就会为当前持有锁的节点注册一个watcher监听器
3、第三个线程进来发现锁已经被持有了,因为是顺序节点的缘故,就会为上一个节点去创建一个watcher监听器
4、当第一个线程释放锁后,删除节点,由它的下一个节点去占有锁
看到这里,聪明的小伙伴们都已经看出来顺序节点的好处了。非顺序节点的话,每进来一个线程进来都会去持有锁的节点上注册一个监听器,容易引发“羊群效应”。
这么大一群羊一起向你飞奔而来,不管你顶不顶得住,反正zk服务器是会增大宕机的风险。而顺序节点的话就不会,顺序节点当发现已经有线程持有锁后,会向它的上一个节点注册一个监听器,这样当持有锁的节点释放后,也只有持有锁的下一个节点可以抢到锁,相当于是排好队来执行的,降低服务器宕机风险。
至于为什么使用临时节点,和redis的过期时间一个道理,就算zk服务器宕机,临时节点会随着服务器的宕机而消失,避免了死锁的情况。
下面来上一段代码的实现
public class ZooKeeperDistributedLock implements Watcher {
private ZooKeeper zk;
private String locksRoot = "/locks";
private String productId;
private String waitNode;
private String lockNode;
private CountDownLatch latch;
private CountDownLatch connectedLatch = new CountDownLatch(1);
private int sessionTimeout = 30000;
public ZooKeeperDistributedLock(String productId) {
this.productId = productId;
try {
String address = "192.168.189.131:2181,192.168.189.132:2181";
zk = new ZooKeeper(address, sessionTimeout, this);
connectedLatch.await();
} catch (IOException e) {
throw new LockException(e);
} catch (KeeperException e) {
throw new LockException(e);
} catch (InterruptedException e) {
throw new LockException(e);
}
}
public void process(WatchedEvent event) {
if (event.getState() == KeeperState.SyncConnected) {
connectedLatch.countDown();
return;
}
if (this.latch != null) {
this.latch.countDown();
}
}
public void acquireDistributedLock() {
try {
if (this.tryLock()) {
return;
} else {
waitForLock(waitNode, sessionTimeout);
}
} catch (KeeperException e) {
throw new LockException(e);
} catch (InterruptedException e) {
throw new LockException(e);
}
}
//获取锁
public boolean tryLock() {
try {
// 传入进去的locksRoot + “/” + productId
// 假设productId代表了一个商品id,比如说1
// locksRoot = locks
// /locks/10000000000,/locks/10000000001,/locks/10000000002
lockNode = zk.create(locksRoot + "/" + productId, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// 看看刚创建的节点是不是最小的节点
// locks:10000000000,10000000001,10000000002
List<String> locks = zk.getChildren(locksRoot, false);
Collections.sort(locks);
if(lockNode.equals(locksRoot+"/"+ locks.get(0))){
//如果是最小的节点,则表示取得锁
return true;
}
//如果不是最小的节点,找到比自己小1的节点
int previousLockIndex = -1;
for(int i = 0; i < locks.size(); i++) {
if(lockNode.equals(locksRoot + “/” + locks.get(i))) {
previousLockIndex = i - 1;
break;
}
}
this.waitNode = locks.get(previousLockIndex);
} catch (KeeperException e) {
throw new LockException(e);
} catch (InterruptedException e) {
throw new LockException(e);
}
return false;
}
private boolean waitForLock(String waitNode, long waitTime) throws InterruptedException, KeeperException {
Stat stat = zk.exists(locksRoot + "/" + waitNode, true);
if (stat != null) {
this.latch = new CountDownLatch(1);
this.latch.await(waitTime, TimeUnit.MILLISECONDS);
this.latch = null;
}
return true;
}
//释放锁
public void unlock() {
try {
System.out.println("unlock " + lockNode);
zk.delete(lockNode, -1);
lockNode = null;
zk.close();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
//异常
public class LockException extends RuntimeException {
private static final long serialVersionUID = 1L;
public LockException(String e) {
super(e);
}
public LockException(Exception e) {
super(e);
}
}
}
小结
既然明白了redis和zk分别对分布式锁的实现,那么总该有所不同的吧。没错,我都帮大家整理好了。
1、实现方式的不同,redis实现为去插入一条占位数据,而zk实现为去注册一个临时节点。
2、遇到宕机情况时,redis需要等到过期时间到了后自动释放锁,而zk因为是临时节点,在宕机时候已经是删除了节点去释放锁。
3、redis在没抢占到锁的情况下一般会去自旋获取锁,比较浪费性能,而zk是通过注册监听器的方式获取锁,性能而言优于redis。
不过具体要采用哪种实现方式,还是需要具体情况具体分析,结合项目引用的技术栈来落地实现。
完!
都看到着了,点个赞点个关注再走呗~