优化:性能优化
基本实现中由于无限自旋影响性能:
试想:每个请求要想正常的执行完成,最终都是要创建节点,如果能够避免争抢必然可以提高性能。这里借助于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. 公平锁:有序性节点