写在最前面

前几周写了篇 利用Redis实现分布式锁 ,今天简单总结下ZooKeeper实现分布式锁的过程。其实生产上我只用过Redis或者数据库的方式,之前还真没了解过ZooKeeper怎么实现分布式锁。这周简单写了个小Demo,更坚定了我继续使用Redis的信心了。

ZooKeeper分布式锁的实现原理

在分布式解决方案中,Zookeeper是一个分布式协调工具。当多个JVM客户端,同时在ZooKeeper上创建相同的一个临时节点,因为临时节点路径是保证唯一,只要谁能够创建节点成功,谁就能够获取到锁。没有创建成功节点,就会进行等待,当释放锁的时候,采用事件通知给客户端重新获取锁资源。如果请求超时直接返回给客户端超时,重新请求即可。

代码实现

为了更好的展现效果,我这里设置每个线程请求需要1s,请求超时时间为30s。

首先我们先写一个测试类,模拟多线程多客户端请求的情况:

public class ZkLockTest implements Runnable {
private ZkLock zkLock = new ZkDistributedLock(); public void run() {
try {
if (zkLock.getLock((long)30000,null)) {
System.out.println("线程:" + Thread.currentThread().getName() + ",抢购成功:" + System.currentTimeMillis());
} else {
System.out.println("线程:" + Thread.currentThread().getName() + ",抢购超时失败请重试:" + System.currentTimeMillis());
}
Thread.sleep(1000);
} catch (Exception e) { } finally {
zkLock.unLock();
}
} public static void main(String[] args) {
System.out.println("zk分布式锁开始。。");
for (int i = 0; i < 100; i++) {
new Thread(new ZkLockTest()).start();
}
} }

模拟100个线程,去同时争夺锁。当然上述写法 100个线程不会同时启动,如果需要的话可以用信号量的形式控制。

其次,写一个锁的接口

public interface ZkLock {

    // 获取锁
Boolean getLock(Long acquireTimeout,Long endTime); // 释放锁
void unLock();
}

这里我定义了两个接口,分别对应获取锁和释放锁。

在获取锁中有两个参数,含义分别为锁超时时间和最终计算的超时时间,具体看下文代码就懂了。

public class ZkDistributedLock implements ZkLock {
// 集群连接地址
private String CONNECTION = "127.0.0.1:2181";
// zk客户端连接
private ZkClient zkClient = new ZkClient(CONNECTION);
// path路径
private String lockPath = "/lock";
private CountDownLatch countDownLatch;
//请求设置的超时时间:acquireTimeout 毫秒。最终超时时间endTime
public Boolean getLock(Long acquireTimeout,Long endTime) {
Boolean lock = false;
if (endTime == null) {
//等待超时时间
endTime = System.currentTimeMillis() + acquireTimeout;
}
if (tryLock()) {
System.out.println("####获取锁成功######");
lock = true;
} else {
if (waitLock(endTime)) {
if (getLock(null,endTime)) {
lock = true;
}
}
}
return lock;
} public void unLock() {
if (zkClient != null) {
System.out.println("#######释放锁#########");
zkClient.close();
}
} private boolean tryLock() {
try {
zkClient.createEphemeral(lockPath);
return true;
} catch (Exception e) { return false;
} } private Boolean waitLock(Long endTime) {
// System.out.println("进入等待");
// 使用zk临时事件监听
IZkDataListener iZkDataListener = null;
try {
// 使用zk临时事件监听
iZkDataListener = new IZkDataListener() { public void handleDataDeleted(String path) throws Exception {
if (countDownLatch != null) {
countDownLatch.countDown();
}
}
public void handleDataChange(String arg0, Object arg1) throws Exception { }
};
// 注册事件通知
zkClient.subscribeDataChanges(lockPath, iZkDataListener);
if (System.currentTimeMillis() < endTime) {
if (zkClient.exists(lockPath)) {
countDownLatch = new CountDownLatch(1);
try {
countDownLatch.await();
return true;
} catch (Exception e) { }
} else {
return true;
}
} else {
System.out.println("超时返回");
}
} catch (Exception e) { } finally {
// 监听完毕后,移除事件通知
zkClient.unsubscribeDataChanges(lockPath, iZkDataListener);
}
return false;
} }

这个类是我实现zk锁的核心类,和上文原理图中类似。首先用户请求的时候需要获取锁,第一个争夺到锁的用户执行相关逻辑后释放锁,在这个过程中如果程序出错断开连接,因为临时节点的缘故,节点也会自动删除释放锁的。

另外就是其他争夺锁失败的用户,我这里设置了一定的等待时间,当在时间内原锁释放,还是可以重新去获取锁的。这里要说下锁释放的监听,在原生的zookeeper中,使用watcher需要每次先注册,而且使用一次就需要注册一次。而在zkClient中,没有注册watcher的必要,而是引入了listener的概念,即只要client在某一个节点中注册了listener,只要服务端发生变化,就会通知当前注册listener的客户端。我这里使用的是IZkDataListener,这个类是zkClient提供的一个接口,它可以在当前节点数据内容或版本发生变化或者当前节点被删除时触发。

触发后我们就可以重新去争夺锁,当再次争夺失败进入等待时会再次检测当前请求是否超时。

下面我们来看下上述代码的实现效果:

zk分布式锁开始。。
####获取锁成功######
线程:Thread-3,抢购成功:1544183770509
#######释放锁#########
####获取锁成功######
线程:Thread-81,抢购成功:1544183771555
#######释放锁######### ......... 超时返回
线程:Thread-11,抢购超时失败请重试:1544183800677
超时返回
线程:Thread-1,抢购超时失败请重试:1544183800681
#######释放锁#########
#######释放锁#########
####获取锁成功######
线程:Thread-49,抢购成功:1544183801710
超时返回
线程:Thread-25,抢购超时失败请重试:1544183801729
超时返回
#######释放锁#########
#######释放锁#########

释放锁说的可能并不准确,应该说是关闭连接,有些线程实际上是没有得到锁的。

简单尝试了下zk实现分布式锁的方式,当然上述代码如果应用到生产中肯定问题还是不少的,因为兴趣点不在这,就不仔细研究了。简单来说,相比其他方式实现步骤更为复杂,感觉更容易出问题。

总结

经过三种方式的应用和简单实践,总结实现分布式锁三种方式的优缺点如下

1、数据库实现:

优点,实现简单只是for update的显示加锁。缺点,性能问题较大,而且本身系统在设计时是需要尽量减轻数据库的压力的。

2、Redis实现:

优点:一般互联网项目都会集成,本身是nosql数据库,缓存实现简单,高并发应付自如,同时新版的Jedis完美解决了以往程序出错,未设置超时时间死锁的问题。

缺点:网络问题可能会引起锁删除失败,超时时间有一定的延迟。

3、ZooKeeper实现:

优点:Zookeeper临时节点先天可控的有效期设置,避免了程序引发的死锁问题

缺点:实现过于繁杂,相比其他两种写法更容易出问题,另外还需要单独维护zk。

结论:

我个人更为推荐Redis的实现方式,实现简单,性能也比较好,同时引入集群可以提高可用性。Jedis多参的设置方式也较好的保证了有效期的控制和死锁的问题。

分布式锁实践(二)-ZooKeeper实现总结的更多相关文章

  1. Springboot分布式锁实践(redis)

    springboot2本地锁实践一文中提到用Guava Cache实现锁机制,但在集群中就行不通了,所以我们还一般要借助类似Redis.ZooKeeper 之类的中间件实现分布式锁,下面我们将利用自定 ...

  2. 分布式锁实践(一)-Redis编程实现总结

    写在最前面 我在之前总结幂等性的时候,写过一种分布式锁的实现,可惜当时没有真正应用过,着实的心虚啊.正好这段时间对这部分实践了一下,也算是对之前填坑了. 分布式锁按照网上的结论,大致分为三种:1.数据 ...

  3. 基于redis 实现分布式锁(二)

    https://blog.csdn.net/xiaolyuh123/article/details/78551345 分布式锁的解决方式 基于数据库表做乐观锁,用于分布式锁.(适用于小并发) 使用me ...

  4. 分布式锁tair redis zookeeper,安全性

    tair分布式锁实现:https://yq.aliyun.com/articles/58928 redis分布式锁:https://www.cnblogs.com/jianwei-dai/p/6137 ...

  5. 分布式锁之一:zookeeper分布式锁1

    zookeeper集群的每个节点的数据都是一致的, 那么我们可以通过这些节点来作为锁的标志. 首先给锁设置一下API, 至少要包含, lock(锁住), unlock(解锁), isLocked(是否 ...

  6. redis分布式锁实践

    分布式锁在多实例部署,分布式系统中经常会使用到,这是因为基于jvm的锁无法满足多实例中锁的需求,本篇将讲下redis如何通过Lua脚本实现分布式锁,不同于网上的redission,完全是手动实现的 我 ...

  7. Redisson实现分布式锁(二)

    本次基于注解+AOP实现分布式锁(招式与前文基于注解切换多数据源相同),话不多说,直接上样例: 首先自定义注解:设计时需要考虑锁的一般属性:keys,最大等待时间,超时时间,时间单位. package ...

  8. 分布式锁之二:zookeeper分布式锁2

    示例: package com.util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.zoo ...

  9. zookeeper — 实现分布式锁

    一.前言 在之前的文章中介绍过分布式锁的特点和利用Redis实现简单的分布式锁.但是分布式锁的实现还有很多其他方式,但是万变不离其宗,始终遵循一个特点:同一时刻只能有一个操作获取.这篇文章主要介绍如何 ...

随机推荐

  1. K-Means & Sequential Leader Clustering

    2017-12-31 19:08:37 k-平均算法源于信号处理中的一种向量量化方法,现在则更多地作为一种聚类分析方法流行于数据挖掘领域.k-means的目的是:把样本划分到k个聚类中,使得每个点都属 ...

  2. 雷林鹏分享:Ruby 文件的输入与输出

    Ruby 文件的输入与输出 Ruby 提供了一整套 I/O 相关的方法,在内核(Kernel)模块中实现.所有的 I/O 方法派生自 IO 类. 类 IO 提供了所有基础的方法,比如 read. wr ...

  3. Rails-Treasure chest1 (自定义Model网址;多语言包; 时区设置, TimeZone类; 格式日期时间; 表单单选UI; 表单多选UI;Select2 Plugin)

    自定义Model网址: 随机值网址SecureRandom.base58 多语言包, 包括默认语言设置和user自设置. 时区设置, TimeZone类 ,增加user自选时区功能 格式日期时间: x ...

  4. BooStrap4文档摘录 2 Content, Component

    Content Reboot:从新写了主要元素的排列. 本章讲了各种元素及其相关的类. ⚠️ 文档左上角有搜索栏. Components Alert✅ Badge✅ Button✅和Button gr ...

  5. ssh 上传文件以及文件夹到linux服务器

    闲来无事分享一篇,帮助到你的话,麻烦给老弟点个关注.经常会分享一些实用技能. 回归正题,现在服务器linux很多.是不是不会传文件?别急 下面就是方法: 一.上传文件到linux服务器 首先从你本地切 ...

  6. 直方图及low_value、high_value

    直方图 Histogram是一种特殊的列统计信息,详细描述了目标列的数据分布情况.存储在数据字典基表 histogram$; 专门为了准确评估分布不均匀的目标列的可选择率.结果集的cardianlit ...

  7. 两个值相等的Integer的==比较问题

    @Test    public void testIntegerEqual() {        /** -128~127 之外的数**/        Integer tem = 129;      ...

  8. UEFI下windows启动过程

    引导文件 在UEFI安装完操作系统后,Windows至少使用两个分区,一个叫做ESP分区(EFI SYSTEM PARTITION),用于存放启动文件,另一个则是BIOS下正常的系统分区,不同的是,B ...

  9. 为了更好更方便地活着——爱上private

    刚开始接触OOP的时候,打心底里我不喜欢private与protected.我声明一个public然后不直接用它,不就跟private一样吗?在某些场合下,我还能偷偷地用一下public变量,这不是更 ...

  10. linux下之mysql篇

    网上查到的一般是 yum install mysql yum install mysql-server yum intall mysql-devel 但是在centos7下  mysql-server ...