细致的看看其它博客:
Zookeeper入门看这篇就够了
https://blog.csdn.net/java_66666/article/details/81015302
【七张图】彻底讲清楚ZooKeeper分布式锁的实现原理
https://www.imooc.com/article/284956?block_id=tuijian_wz
Zookeeper实现分布式锁
https://www.jianshu.com/p/5d12a01018e1
分布式锁的简单理解
我的理解:在分布式系统中,多个被负载均衡的实例,同时使用同一个资源,这种情况资源会被互相干扰,我觉得可以理解为“多系统的线程不安全”。在单个系统中,线程不安全有几种线程安全关键字来处理,在数据库中是sql语句来处理,这些东西都被总结成两种思路1、乐观锁 2、悲观锁。
悲观锁简单来说:我认为我要使用的资源会有人过来使用,所以先锁定起来,让这个资源目前只有我能操作。
乐观锁简单来说:乐观认为,别人没有修改公共数据,我先记住这个版本,我操作完后再对比版本,再看是不是有人操作过。
那么分布式锁,我觉得就可以用悲观锁的形式。又因为你是多个系统,一个系统关键字,总不能另一个系统也能受到影响吧,所以就需要一个外部的标记来确定是否有其它系统的线程来操作这个资源。
这个外部标记的实现可以是人、数据库、zk、redis......
就拿人来说,我们先在多个系统在要访问的资源前面加个断点,系统在跑的时候,到了断点停住,然后我们一个一个系统的断点走完,走完一个系统再另一个系统断点走完,这样是不是一个一个系统都互相不干扰使用同一个资源啊。数据库也是同理,用一条数据作标记,但是明显的数据库做外部标记的问题就多了,我还要用分布式锁保护它呢,用它来做锁?
比喻说完,这就明白zk和redis的用处了,zk是创建节点的形式做标记,redis是判断key的形式来做标记的。
zk 分布式锁原理简单介绍
- 创建一个 zk 临时 node。
- zk 会保证一个 node path 只会被创建一次,如果已经被创建,则抛出 NodeExistsException 。
- 这个时候可以去做业务操作。
- 释放锁,则是删除这个临时 node。
public class ZooKeeperSession {
private ZooKeeper zookeeper;
private CountDownLatch connectedSemaphore = new CountDownLatch(1);
private ZooKeeperSession() {
String connectString = "192.168.1.5:2181,192.168.1.6:2181,192.168.1.7:2181";
int sessionTimeout = 5000;
try {
// 异步连接,所以需要一个 org.apache.zookeeper.Watcher 来通知
// 由于是异步,利用 CountDownLatch 来让构造函数等待
zookeeper = new ZooKeeper(connectString, sessionTimeout, event -> {
Watcher.Event.KeeperState state = event.getState();
System.out.println("watch event:" + state);
if (state == Watcher.Event.KeeperState.SyncConnected) {
System.out.println("zookeeper 已连接");
connectedSemaphore.countDown();
}
if (this.connectedSemaphore != null) {
this.connectedSemaphore.countDown();
}
});
} catch (IOException e) {
e.printStackTrace();
}
try {
connectedSemaphore.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("zookeeper 初始化成功");
}
/**
* 获取分布式锁
*/
public void acquireDistributedLock(Long lockId) {
String path = "/my-lock-" + lockId;
byte[] data = "".getBytes();
try {
// 创建一个临时节点,后面两个参数一个安全策略,一个临时节点类型
// EPHEMERAL:客户端被断开时,该节点自动被删除
zookeeper.create(path, data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
System.out.println("获取锁成功 lock[id=" + lockId + "]");
} catch (Exception e) {
// e.printStackTrace();
System.out.println("锁已被获取lock[id=" + lockId + "] ");
// 如果锁已经被创建,那么将异常
// 循环等待锁的释放
int count = 0;
while (true) {
try {
//TimeUnit.MILLISECONDS.sleep(20);
// 休眠 20 毫秒后再次尝试创建
Stat stat = zookeeper.exists(path, true);
if (stat != null) {
this.connectedSemaphore = new CountDownLatch(1);
this.connectedSemaphore.await(20, TimeUnit.MILLISECONDS);
this.connectedSemaphore = null;
}
zookeeper.create(path, data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
} catch (Exception e1) {
// e1.printStackTrace();
count++;
continue;
}
System.out.println("获取锁成功 lock[id=" + lockId + "] 尝试了 " + count + " 次.");
break;
}
}
}
/**
* 释放分布式锁
*/
public void releaseDistributedLock(Long lockId) {
String path = "/my-lock-" + lockId;
try {
zookeeper.delete(path, -1);
System.out.println("释放锁成功 lock[id=" + lockId + "] ");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
private static ZooKeeperSession instance = new ZooKeeperSession();
public static ZooKeeperSession getInstance() {
return instance;
}
public static void main(String[] args) throws InterruptedException {
ZooKeeperSession instance = ZooKeeperSession.getInstance();
CountDownLatch downLatch = new CountDownLatch(2);
IntStream.of(1, 2).forEach(i -> new Thread(() -> {
instance.acquireDistributedLock(1L);
System.out.println(Thread.currentThread().getName() + " 得到锁并休眠 10 秒");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance.releaseDistributedLock(1L);
System.out.println(Thread.currentThread().getName() + " 释放锁");
downLatch.countDown();
}).start());
downLatch.await();
}
}
zk保证只能创建同一个节点,某个节点尝试创建临时 znode,此时创建成功了就获取了这个锁,这个时候别的客户端来创建锁会失败,所以当第二个线程想要创建就会走cath,然后循环尝试创建,一直到持有锁的线程释放锁。在分布式系统中,因为系统复杂度的提高,出问题的概率就会更多,拿到锁执行中挂掉了怎么办,或者zk挂了怎么办。只能注册个监听器监听这个锁。释放锁就是删除这个 znode,一旦释放掉就会通知客户端,然后有一个等待着的客户端就可以再次重新加锁。
当大量节点监控一个节点,这时候zk又会有另一个问题羊群效应。如果几十个客户端同时争抢一个锁,此时会导致任何一个客户端释放锁的时候,zk反向通知几十个客户端,通知操作会造成zookeeper性能突然下降,几十个客户端又要发送请求到zk去尝试创建锁,所以会发现,几十个客户端要加锁,大家乱糟糟的,无序的,这样会影响zookeeper的使用。
采用另一种方式,创建临时顺序节点:如果有一把锁,被多个人给竞争,此时多个人会排队,第一个拿到锁的人会执行,然后释放锁;后面的每个人都会去监听排在自己前面的那个人创建的 node 上,一旦某个人释放了锁,排在自己后面的人就会被 zookeeper 给通知,一旦被通知了之后,就 ok 了,自己就获取到了锁,就可以执行代码了。
//比较详细的思路讲解(带图片的,肯定看得懂)
http://www.imooc.com/article/284859 。
其实用开源框架就是这点好,方便。这个Curator框架的zk分布式锁的加锁和释放锁的实现原理,其实就是上面我们说的那样子。但是如果你要手动实现一套那个代码的话。还是有点麻烦的,要考虑到各种细节,异常处理等等。所以大家如果考虑用zk分布式锁,可以参考下本文的思路。
这段话说得好,既然说了Curator框架那么有机会去看看。
下面是一段代码创建临时顺序节点的例子,确实有点问题。
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 = 300;
public ZooKeeperDistributedLock(String productId) {
this.productId = productId;
try {
String address = "192.168.1.5:2181,192.168.1.6:2181,192.168.1.7:2181";
zk = new ZooKeeper(address, sessionTimeout, this);
connectedLatch.await();
} catch (IOException e) {
throw new LockException(e);
} catch (InterruptedException e) {
throw new LockException(e);
}
}
public void process(WatchedEvent event) {
if (event.getState() == Watcher.Event.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 {
// 删除/locks/10000000000节点
// 删除/locks/10000000001节点
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);
}
}
private static ZooKeeperDistributedLock instance = new ZooKeeperDistributedLock("12");
public static ZooKeeperDistributedLock getInstance() {
return instance;
}
public static void main(String[] args) throws InterruptedException {
ZooKeeperDistributedLock instance = ZooKeeperDistributedLock.getInstance();
CountDownLatch downLatch = new CountDownLatch(2);
IntStream.of(1, 2).forEach(i -> new Thread(() -> {
instance.acquireDistributedLock();
System.out.println(Thread.currentThread().getName() + " 得到锁并休眠 10 秒");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance.unlock();
System.out.println(Thread.currentThread().getName() + " 释放锁");
downLatch.countDown();
}).start());
downLatch.await();
}
}