作者:潘吉祥
欢迎读者跟着一同来填坑,不知道大家对于分布式锁有没有大概的了解,手撕之前,还是总体性地介绍一下。
分布式锁:用来解决分布式环境中共享数据的正确性。
首先我们来想一下分布式环境下遇到的一个问题:我们希望生成一个全局唯一id,该如何做呢?注意,我们强调的是分布式环境中(不是有雪花算法吗?呃,咱们是抱着学习的心态来的哈)。
传统的单机环境很容易:UUID加上时间戳就OK了。单是分布式环境是绝对不行的,我们来看图:
那么怎么解决这种问题呢,此时传统的锁是毫无用武之地的,那么就需要分布式锁的概念了,还是看图:
上图中所有机器同时指向zookeeper,他们要进行我们预期的id生成,步骤如下:
1.所有机器同时往zookeeper中创建同一个名字的临时节点,但zookeeper能够保证节点的唯一性,已经存在的节点,再去创建就会报错,因此只会有一个机器创建成功
2.创建节点失败的机器需要订阅这个节点变化的消息,并进入阻塞
3.创建节点成功的机器在生成id后,关闭和zookeeper的连接,zookeeper会自动删除它所创建的临时节点
4.当临时节点删除后会发送广播消息,此时订阅了节点变化消息的机器(即创建节点失败的机器)监听到消息,打断当前的阻塞,再次去尝试创建节点(回到第一步),在这批机器中又只会有一个创建成功,剩下的机器又重复上面的步骤。
因此整个流程下来最后能够保证一个时刻只能有一个机器执行生成id的逻辑,因此就能够生成全局的唯一id。
(这里说明一下关于zookeeper不能创同名节点的问题,zookeeper除了临时节点和持久节点之外,还有顺序节点的概念,即你在创建节点的时候可以选择临时的或持久的顺序节点,此时的name可以相同,zookeeper会自动添加递增的序号,本质上也是不同名字的,只是zookeeper帮你做了。。)
下面进入代码实战阶段:
我们定义一个zookeeper锁对象,里面定义lock和unlock方法,lock方法执行的就是创建锁的逻辑,unlock执行的就是关闭zookeeper连接的逻辑。
public class ZkLock {
private ZkClient zkClient = new ZkClient("192.168.1.23:2181");
//锁节点
private String zkLock = "/zk_lock";
//并发工具
private CountDownLatch countDownLatch = null;
//获取锁,即创建节点
public void lock(){
try {
//创建锁节点
zkClient.createEphemeral(zkLock);
}catch (Exception e){
//精确点的异常应该是ZkNodeExistsException
//所有创建失败的都会进入此逻辑
//创建监听
IZkDataListener listener = new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception { }
//如果节点删除了就不再阻塞
@Override
public void handleDataDeleted(String s) throws Exception {
if(countDownLatch != null){
countDownLatch.countDown();
}
}
};
//订阅节点变化消息
zkClient.subscribeDataChanges(zkLock,listener);
countDownLatch = new CountDownLatch(1);
try {
//在节点未被删除之前都处于阻塞状态
countDownLatch.await();
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
//当执行到此,说明监听已经使用,已经完成一轮操作,要清除监听
zkClient.unsubscribeDataChanges(zkLock,listener);
//上一次没抢到的节点此时已经释放,可以重新抢着创建,递归
lock();
}
}
//释放锁
public void unlock(){
if(zkClient != null){
//当执行了close,临时节点救护自动被删除
zkClient.close();
System.out.println("释放锁");
}
}
}
测试:
public class Test{
public static void main(String[] args) {
//开启十个线程模拟十台不同的机器在生产id
for (int i = 0; i < 10; i++) {
new Thread(() ->{
//创建我们定义的分布式锁
ZkLock zkLock = new ZkLock();
//加锁
zkLock.lock();
String id = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(id);
//完成业务逻辑之后释放锁
zkLock.unlock();
}).start();
}
//没有加zookeeper锁的
// for (int i = 0; i < 10; i++) {
// new Thread(() ->{
//
// String id = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date());
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// System.out.println(id);
//
// }).start();
// }
}
}
加锁的输出:
2020-04-07-16-28-56
释放锁
2020-04-07-16-28-57
释放锁
……
释放锁
2020-04-07-16-29-04
释放锁
2020-04-07-16-29-05
释放锁
未加锁:
2020-04-07-17-50-02
2020-04-07-17-50-02
2020-04-07-17-50-02
……
2020-04-07-17-50-02
这里用多个线程模拟了多个机器,注意我们没有使用任何Java锁,而达到了顺序执行的效果。
关于zookeeper分布式锁,我们重在理解分布式锁产生的背景和如何利用zookeeper达到分布式锁的四个步骤,怎么样,你也赶紧试试吧!
Ps:代码中用到了一个JUC工具CountDownLatch,使用它可以让一个线程等待其它指定的线程走完逻辑之后再执行自己的方法。
与它类似的还有CyclicBarrier工具,如需推文学习,留言即可哈,让你秒懂。
Git地址:https://github.com/rockit-ba/zookeeper-lock.git)
END
【推荐阅读】
贼好用的Java工具类库,GitHub星标10k+,你在用吗?
感谢阅读,请扫码关注
明天见