基于zookeeper实现分布式锁(下)

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
云原生网关 MSE Higress,422元/月
注册配置 MSE Nacos/ZooKeeper,118元/月
简介: 基于zookeeper实现分布式锁(下)

优化:性能优化

基本实现中由于无限自旋影响性能:

试想:每个请求要想正常的执行完成,最终都是要创建节点,如果能够避免争抢必然可以提高性能。这里借助于zk的临时序列化节点,实现分布式锁:

实现阻塞锁

代码实现:

1. public class ZkDistributedLock {
2. public static final String ROOT_PATH = "/distribute";
3. private String path;
4. private ZooKeeper zooKeeper;
5. 
6. 
7. public ZkDistributedLock(ZooKeeper zooKeeper, String lockname) {
8. this.zooKeeper = zooKeeper;
9. try {
10. this.path = zooKeeper.create(ROOT_PATH + "/" + lockname + "_",
11. null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);
12.         } catch (KeeperException e) {
13.             e.printStackTrace();
14.         } catch (InterruptedException e) {
15.             e.printStackTrace();
16.         }
17.     }
18. 
19. public void lock() {
20. String preNode = getpreNode(path);
21. //如果该节点没有前一个节点,说明该节点是最小的节点
22. if (StringUtils.isEmpty(preNode)) {
23. return;
24.         }
25. //重新检查是否获取到锁
26. try {
27.             Thread.sleep(20);
28.             lock();
29.         } catch (InterruptedException e) {
30.             e.printStackTrace();
31.         }
32.     }
33. 
34. /**
35.      * 获取指定节点的前节点
36.      *
37.      * @param path
38.      * @return
39.      */
40. private String getpreNode(String path) {
41. //获取当前节点的序列化序号
42. Long curSerial = Long.valueOf(StringUtil.substringAfter(path, '_'));
43. //获取根路径下的所有序列化子节点
44. try {
45.             List<String> nodes = this.zooKeeper.getChildren(ROOT_PATH, false);
46. //判空处理
47. if (CollectionUtils.isEmpty(nodes)) {
48. return null;
49.             }
50. //获取前一个节点
51. Long flag = 0L;
52. String preNode = null;
53. for (String node : nodes) {
54. //获取每个节点的序列化号
55. Long serial = Long.valueOf(StringUtil.substringAfter(path, '_'));
56. if (serial < curSerial && serial > flag) {
57.                     flag = serial;
58.                     preNode = node;
59.                 }
60.             }
61. return preNode;
62.         } catch (KeeperException e) {
63.             e.printStackTrace();
64.         } catch (InterruptedException e) {
65.             e.printStackTrace();
66.         }
67. return null;
68.     }
69. 
70. public void unlock() {
71. try {
72. this.zooKeeper.delete(path, 0);
73.         } catch (InterruptedException e) {
74.             e.printStackTrace();
75.         } catch (KeeperException e) {
76.             e.printStackTrace();
77.         }
78. 
79.     }
80. }

主要修改了构造方法和lock方法:

并添加了getPreNode获取前置节点的方法。

测试结果如下:

性能反而更弱了。

原因:虽然不用反复争抢创建节点了,但是会自选判断自己是最小的节点,这个判断逻辑反而更复杂更 耗时。

解决方案:监听实现阻塞锁

监听实现阻塞锁

对于这个算法有个极大的优化点:假如当前有1000个节点在等待锁,如果获得锁的客户端释放锁时,这1000个客户端都会被唤醒,这种情况称为“羊群效应”;在这种羊群效应中,zookeeper需要通知1000个 客户端,这会阻塞其他的操作,最好的情况应该只唤醒新的最小节点对应的客户端。应该怎么做呢?在 设置事件监听时,每个客户端应该对刚好在它之前的子节点设置事件监听,例如子节点列表 为/lock/lock-0000000000、/lock/lock-0000000001、/lock/lock-0000000002,序号为1的客户端监听 序号为0的子节点删除消息,序号为2的监听序号为1的子节点删除消息。

所以调整后的分布式锁算法流程如下:

  • 客户端连接zookeeper,并在/lock下创建临时的且有序的子节点,第一个客户端对应的子节点 为/lock/lock-0000000000,第二个为/lock/lock-0000000001,以此类推;
  • 客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子 节点,如果是则认为获得锁,否则监听刚好在自己之前一位的子节点删除消息,获得子节点变更通 知后重复此步骤直至获得锁;
  • 执行业务代码;
  • 完成业务流程后,删除对应的子节点释放锁。

改造ZkDistributedLock的lock方法:

1. public void lock() {
2. String preNode = getpreNode(path);
3. //如果该节点没有前一个节点,说明该节点是最小的节点
4. if (StringUtils.isEmpty(preNode)) {
5. return;
6.         } else {
7. CountDownLatch countDownLatch = new CountDownLatch(1);
8. try {
9. if (this.zooKeeper.exists(ROOT_PATH + "/" + preNode, watchedEvent -> {
10.                     countDownLatch.countDown();
11.                 }) == null) {
12. return;
13.                 }
14.                 countDownLatch.await();
15. return;
16. 
17.             } catch (KeeperException e) {
18.                 e.printStackTrace();
19.             } catch (InterruptedException e) {
20.                 e.printStackTrace();
21.             }
22. try {
23.                 Thread.sleep(200);
24.             } catch (InterruptedException e) {
25.                 e.printStackTrace();
26.             }
27.             lock();
28.         }
29.     }

压力测试效果如下:

由此可见性能提高不少仅次于redis的分布式锁

优化:可重入锁

引入ThreadLocal线程局部变量保证zk分布式锁的可重入性。

在对应的线程的存储数据

1. public class ZkDistributedLock {
2. public static final String ROOT_PATH = "/distribute";
3. private String path;
4. private ZooKeeper zooKeeper;
5. private static final ThreadLocal<Integer> THREAD_LOCAL = new ThreadLocal<>();
6. 
7. 
8. public ZkDistributedLock(ZooKeeper zooKeeper, String lockname) {
9. this.zooKeeper = zooKeeper;
10. try {
11. this.path = zooKeeper.create(ROOT_PATH + "/" + lockname + "_",
12. null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);
13.         } catch (KeeperException e) {
14.             e.printStackTrace();
15.         } catch (InterruptedException e) {
16.             e.printStackTrace();
17.         }
18.     }
19. 
20. public void lock() {
21. Integer flag = THREAD_LOCAL.get();
22. if (flag != null && flag > 0) {
23.             THREAD_LOCAL.set(flag + 1);
24. return;
25.         }
26. String preNode = getpreNode(path);
27. //如果该节点没有前一个节点,说明该节点是最小的节点
28. if (StringUtils.isEmpty(preNode)) {
29. return;
30.         } else {
31. CountDownLatch countDownLatch = new CountDownLatch(1);
32. try {
33. if (this.zooKeeper.exists(ROOT_PATH + "/" + preNode, watchedEvent -> {
34.                     countDownLatch.countDown();
35.                 }) == null) {
36. return;
37.                 }
38.                 countDownLatch.await();
39.                 THREAD_LOCAL.set(1);
40. return;
41. 
42.             } catch (KeeperException e) {
43.                 e.printStackTrace();
44.             } catch (InterruptedException e) {
45.                 e.printStackTrace();
46.             }
47. try {
48.                 Thread.sleep(200);
49.             } catch (InterruptedException e) {
50.                 e.printStackTrace();
51.             }
52.             lock();
53.         }
54.     }
55. 
56. /**
57.      * 获取指定节点的前节点
58.      *
59.      * @param path
60.      * @return
61.      */
62. private String getpreNode(String path) {
63. //获取当前节点的序列化序号
64. Long curSerial = Long.valueOf(StringUtil.substringAfter(path, '_'));
65. //获取根路径下的所有序列化子节点
66. try {
67.             List<String> nodes = this.zooKeeper.getChildren(ROOT_PATH, false);
68. //判空处理
69. if (CollectionUtils.isEmpty(nodes)) {
70. return null;
71.             }
72. //获取前一个节点
73. Long flag = 0L;
74. String preNode = null;
75. for (String node : nodes) {
76. //获取每个节点的序列化号
77. Long serial = Long.valueOf(StringUtil.substringAfter(path, '_'));
78. if (serial < curSerial && serial > flag) {
79.                     flag = serial;
80.                     preNode = node;
81.                 }
82.             }
83. return preNode;
84.         } catch (KeeperException e) {
85.             e.printStackTrace();
86.         } catch (InterruptedException e) {
87.             e.printStackTrace();
88.         }
89. return null;
90.     }
91. 
92. public void unlock() {
93. try {
94.             THREAD_LOCAL.set(THREAD_LOCAL.get() - 1);
95. if (THREAD_LOCAL.get() == 0) {
96. this.zooKeeper.delete(path, 0);
97.                 THREAD_LOCAL.remove();
98.             }
99. 
100.         } catch (InterruptedException e) {
101.             e.printStackTrace();
102.         } catch (KeeperException e) {
103.             e.printStackTrace();
104.         }
105. 
106.     }
107. }

zk分布式锁小结

参照redis分布式锁的特点:

1. 互斥 排他:zk节点的不可重复性,以及序列化节点的有序性

2. 防死锁:

       1. 可自动释放锁:临时节点

       2. 可重入锁:借助于ThreadLocal

3. 防误删:临时节点

4. 加锁/解锁要具备原子性

5. 单点问题:使用Zookeeper可以有效的解决单点问题,ZK一般是集群部署的。

6. 集群问题:zookeeper集群是强一致性的,只要集群中有半数以上的机器存活,就可以对外提供服务。

7. 公平锁:有序性节点

相关实践学习
基于MSE实现微服务的全链路灰度
通过本场景的实验操作,您将了解并实现在线业务的微服务全链路灰度能力。
相关文章
|
2月前
|
安全 应用服务中间件 API
微服务分布式系统架构之zookeeper与dubbo-2
微服务分布式系统架构之zookeeper与dubbo-2
|
2月前
|
负载均衡 Java 应用服务中间件
微服务分布式系统架构之zookeeper与dubbor-1
微服务分布式系统架构之zookeeper与dubbor-1
|
1月前
|
分布式计算 NoSQL Java
Hadoop-32 ZooKeeper 分布式锁问题 分布式锁Java实现 附带案例和实现思路代码
Hadoop-32 ZooKeeper 分布式锁问题 分布式锁Java实现 附带案例和实现思路代码
43 2
|
1月前
|
分布式计算 Hadoop
Hadoop-27 ZooKeeper集群 集群配置启动 3台云服务器 myid集群 zoo.cfg多节点配置 分布式协调框架 Leader Follower Observer
Hadoop-27 ZooKeeper集群 集群配置启动 3台云服务器 myid集群 zoo.cfg多节点配置 分布式协调框架 Leader Follower Observer
47 1
|
2月前
分布式-Zookeeper-数据订阅
分布式-Zookeeper-数据订阅
|
2月前
|
监控
分布式-Zookeeper-Zab协议
分布式-Zookeeper-Zab协议
|
1月前
|
存储 SQL 消息中间件
Hadoop-26 ZooKeeper集群 3台云服务器 基础概念简介与环境的配置使用 架构组成 分布式协调框架 Leader Follower Observer
Hadoop-26 ZooKeeper集群 3台云服务器 基础概念简介与环境的配置使用 架构组成 分布式协调框架 Leader Follower Observer
47 0
|
2月前
|
Java
分布式-Zookeeper-分布式锁
分布式-Zookeeper-分布式锁
|
2月前
|
存储 负载均衡 算法
分布式-Zookeeper-Master选举
分布式-Zookeeper-Master选举
|
2月前
|
存储 负载均衡 Dubbo
分布式-Zookeeper(一)
分布式-Zookeeper(一)

热门文章

最新文章