通过中间件zookeeper实现分布式锁,也能支持高性能高并发

本文介绍了如何利用Zookeeper创建临时顺序节点实现分布式锁,详细阐述了不同节点类型及其作用,探讨了避免死锁和减少资源浪费的方法,总结了Zookeeper分布式锁的优势。

锁是为了在多线程的场景中保证数据安全而增加的一种手段,Java中常用的有CountdownLatch,ReentrantLock等单应用中的锁,在现在处处都是分布式的场景需求下就不能满足了,所以就出现了分布式锁。

不同的物理节点有各自的线程,但是他们会访问同一个资源,但是不允许同一时刻访问,所以就有了分布式锁

例如

我们可以通过数据库编写sql来实现分布式锁,但是这种在高并发下性能会出问题,

还有常用的redis实现分布式锁,这个是我们用的最多的一种高性能高并发的实现方式。

今天介绍的一种是通过中间件zookeeper实现分布式锁,也是支持高性能高并发的。

想想实现一个锁想到哪些关键点 ?

争抢锁:只有一个人可以获取锁 获得锁的节点挂了,临时节点 会自动释放 获得锁的人,可以主动释放锁 锁被释放,删除 其他人怎么知道 主动轮训,监听心跳:存在延迟,节点多的情况话压力很大。

根据zk的节点看看是否满足锁

创建持久化节点

zk是创建节点保存数据的,相同节点只允许创建一次,所以我们可以通过成功创建节点实现获取锁的情况。

关键代码

zk.create("/lock", threadId.getBytes() , ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); 复制代码

结果:

如图只有一个线程获取锁,其他线程都出现异常:NodeExists for /lock ,可以保证同一时刻只有一个线程获取锁。然后获得锁的线程逻辑执行结束后应该删除锁。

存在的问题:如果线程崩溃了,锁就无法释放了,最终导致死锁

持久化节点不行,持久化顺序节点自然也不行了

创建临时节点

临时节点:在客户端断开连接的时候就会自动删除

关键代码:

zk.create("/lock", threadId.getBytes() , ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); 复制代码

CreateMode.EPHEMERAL意思就是创建临时节点 ,也就是当线程崩溃,无法主动释放锁的时候,会自动删除,避免死锁。

但是

还存在的问题:没有获取锁的线程会出现错误,则需要不断重试,通过死循环直到获取锁。

临时节点+watch

watch:设置监听回调,当监听的节点或其子节点有变更,则会通知客户端,可参考上一篇

关键代码

zk.create("/lock", threadId.getBytes() , ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); zk.getChildren("/testLock",true, this,"ajisun"); 复制代码

getChildren 获取父节点/testlock下的子节点并设置监听。当子节点/lock被删除 就会触发回调,再次创建节点。

通过watch 避免使用死循环设置堵塞,看似还不错哦。

但是

还还存在问题:所有客户端都去监听同一个父节点,当锁释放的时候,也会通知所有的客户端,带来的压力还是很大。

创建临时顺序节点+watch

临时顺序节点:每个线程都会创建一个临时且有序的节点,互相不冲突

代码如下,十个线程模拟十个客户端

public static void main(String[] args) throws KeeperException, InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(1); ZooKeeper zk = null; try { zk = new ZooKeeper("127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183/testLock", 3000, new DefaultWatch().setCountDownLatch(countDownLatch)); countDownLatch.await(); } catch (Exception e) { e.printStackTrace(); } CountDownLatch countDown = new CountDownLatch(10); for (int i = 0; i < 10; i++) { ZooKeeper finalZk = zk; new Thread() { @Override public void run() { String threadId = Thread.currentThread().getId() + ""; try { finalZk.create("/lock", threadId.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); } catch (Exception e) { e.printStackTrace(); } countDown.countDown(); } }.start(); } try { countDown.await(); } catch (InterruptedException e) { e.printStackTrace(); } List<String> list = zk.getChildren("/", false); list.forEach(s -> System.out.println(s)); } 复制代码

CreateMode.EPHEMERAL_SEQUENTIAL意思就是创建临时顺序节点。

输出节点如下

lock0000000110 lock0000000114 lock0000000113 lock0000000112 lock0000000111 lock0000000107 lock0000000106 lock0000000105 lock0000000109 lock0000000108 复制代码

每次只有一个客户端可以加锁成功,如果同时有100个客户端,当其中一个释放锁后,通知剩下99个客户端,然后99个客户端同时抢锁,其实只有一个会成功,剩下的98个只是陪跑的,做无用功,白白浪费系统资源。

既然每次只有一个会加锁成功,当一个客户端释放锁的时候,只通知一个客户端不就可以了吗。

怎么做到呢?

就是用到临时顺序节点这个特点,不在监听父节点,而是监听前一个节点。

首先创建节点成功后,获取父节点下的所有子节点,因为各个节点是有顺序,可以按照从小到大的顺序排列后,然后判断自己的节点是不是最小的,如果是则获取锁,不是则监听前一个节点。

关键代码如下

public class WatchCallBack implements Watcher, AsyncCallback.StringCallback, AsyncCallback.Children2Callback, AsyncCallback.StatCallback { ZooKeeper zk; String threadId; CountDownLatch cc = new CountDownLatch(1); String pathName; // set/get省略 public void tryLock() { try { // 创建临时有序的锁 zk.create("/lock", threadId.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL, this, "abc"); cc.await(); } catch (InterruptedException e) { e.printStackTrace(); } } public void unLock() { try { zk.delete(pathName, -1); } catch (InterruptedException e) { e.printStackTrace(); } catch (KeeperException e) { e.printStackTrace(); } } ​ @Override public void process(WatchedEvent event) { // 如果第一个锁释放了,只有第二个收到回调事件 // 如果是其他的挂了,对应的后一个也能收到通知 switch (event.getType()) { case None: break; case NodeCreated: break; case NodeDeleted: zk.getChildren("/", false, this, "bcd"); break; case NodeDataChanged: break; case NodeChildrenChanged: break; } } // create call back @Override public void processResult(int rc, String path, Object ctx, String name) { if (name != null) { System.out.println(threadId + "create node:" + name); pathName = name; // 获取所有创建的目录,即参与锁争夺的线程 zk.getChildren("/", false, this, "bcd"); } } ​ /** * getChildren call back * pathName= /lock00000000003 * children=[lock0000000002,lock0000000008,lock0000000005,lock0000000003] * 进入这个回调之后 说明已经创建节点成功,能够看的已经创建的所有节点 */ ​ @Override public void processResult(int rc, String path, Object ctx, List<String> children, Stat stat) { ​ Collections.sort(children); // 所在位置 int i = children.indexOf(pathName.substring(1)); ​ // 判断是不是第一个 if (i == 0) { System.out.println(threadId + " first"); cc.countDown(); } else { // 不是第一个则监控前一个是否存在,如果前一个删除了需要回调我这个session zk.exists("/" + children.get(i - 1), this, this, "xyz"); } } ​ /** * @param rc * @param path * @param ctx * @param stat */ @Override public void processResult(int rc, String path, Object ctx, Stat stat) { ​ } } 复制代码

调用方:

public void lock() { for (int i = 0; i < 10; i++) { new Thread() { @Override public void run() { WatchCallBack watchCallBack = new WatchCallBack(); watchCallBack.setZk(zk); String threadId = Thread.currentThread().getId() + ""; watchCallBack.setThreadId(threadId); // 抢锁 watchCallBack.tryLock(); //干活 System.out.println(threadId + " working......"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } //释放锁 watchCallBack.unLock(); } }.start(); } } 复制代码

总结

zk是通过临时节点,避免死锁问题(session消失,节点消失,锁释放)

通过顺序节点,实现阻塞功能(临时顺序的节点数据)。

通过watch,watch前一个节点,最小的获得锁,一旦最小的锁释放,zk只会给下一个节点回调。避免了抢锁带来的不必要的损耗和压力。

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值