You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

README.md 11 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. ## spring-boot-demo-distributed-lock-zookeeper
  2. > 此 demo 主要演示了 Spring Boot 如何基于 Zookeeper 原生客户端实现一个分布式锁,支持可重入
  3. ## 1.开发步骤
  4. 在 `demo-distributed-lock-api` 模块中,已经实现了基于 AOP 的分布式锁注解拦截、简单的扣减库存案例,因此本模块只需要实现以下两个接口即可。
  5. - `com.xkcoding.distributed.lock.api.DistributedLock`
  6. - `com.xkcoding.distributed.lock.api.DistributedLockClient`
  7. ### 1.1.添加依赖
  8. ```xml
  9. <dependencies>
  10. <dependency>
  11. <groupId>com.xkcoding</groupId>
  12. <artifactId>common-tools</artifactId>
  13. </dependency>
  14. <dependency>
  15. <groupId>com.xkcoding</groupId>
  16. <artifactId>demo-distributed-lock-api</artifactId>
  17. <version>1.0.0-SNAPSHOT</version>
  18. </dependency>
  19. <dependency>
  20. <groupId>org.apache.zookeeper</groupId>
  21. <artifactId>zookeeper</artifactId>
  22. <version>${zookeeper.version}</version>
  23. </dependency>
  24. <dependency>
  25. <groupId>org.projectlombok</groupId>
  26. <artifactId>lombok</artifactId>
  27. <optional>true</optional>
  28. </dependency>
  29. </dependencies>
  30. ```
  31. ### 1.2.代码实现
  32. #### 1.2.1.ZookeeperDistributedLock
  33. > 基于 Zookeeper 实现分布式锁,主要是通过创建并监听临时有序节点 + ThreadLocal 数据结构实现可重入性
  34. ```java
  35. public class ZookeeperDistributedLock extends DistributedLock {
  36. private final ZooKeeper zooKeeper;
  37. private final String lockRootPath;
  38. private final String lockNodePath;
  39. private final ThreadLocal<Integer> LOCK_TIMES = new ThreadLocal<>();
  40. protected ZookeeperDistributedLock(ZkClient client, String lockKey, long lockTime, TimeUnit timeUnit) {
  41. super(lockKey, lockTime, timeUnit);
  42. this.lockRootPath = client.getLockRootPath();
  43. this.zooKeeper = client.getZooKeeper();
  44. try {
  45. // 直接创建临时顺序节点
  46. this.lockNodePath = this.zooKeeper.create(client.getLockRootPath() + "/" + lockKey + "-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE,
  47. CreateMode.EPHEMERAL_SEQUENTIAL);
  48. } catch (KeeperException | InterruptedException e) {
  49. throw new RuntimeException(e);
  50. }
  51. }
  52. @Override
  53. public void lock() {
  54. // 如果是锁重入,则次数加 1,然后直接返回
  55. Integer lockTimes = LOCK_TIMES.get();
  56. if (lockTimes != null && lockTimes > 0L) {
  57. LOCK_TIMES.set(lockTimes + 1);
  58. return;
  59. }
  60. try {
  61. // 当前节点是第一个临时节点,如果是,直接获得锁
  62. String previousLockNodePath = getPreviousNodePath(lockNodePath);
  63. if (StrUtil.isBlank(previousLockNodePath)) {
  64. LOCK_TIMES.set(1);
  65. return;
  66. } else {
  67. // 如果不是第一个临时节点,则给它的前一个临时节点添加监听删除事件(即锁释放)
  68. CountDownLatch wait = new CountDownLatch(1);
  69. Stat stat = this.zooKeeper.exists(lockRootPath + "/" + previousLockNodePath, new Watcher() {
  70. @Override
  71. public void process(WatchedEvent event) {
  72. if (Event.EventType.NodeDeleted == event.getType()) {
  73. // 节点删除,通知获得锁,避免自旋判断影响性能
  74. wait.countDown();
  75. }
  76. }
  77. });
  78. // 未被删除,则需要阻塞
  79. if (stat != null) {
  80. wait.await();
  81. }
  82. // 获得锁,则重入次数设置为 1
  83. LOCK_TIMES.set(1);
  84. }
  85. } catch (Exception e) {
  86. try {
  87. TimeUnit.MILLISECONDS.sleep(50);
  88. } catch (InterruptedException ex) {
  89. throw new RuntimeException(ex);
  90. }
  91. // 重新尝试获取锁
  92. lock();
  93. }
  94. }
  95. @Override
  96. public boolean tryLock() {
  97. throw new UnsupportedOperationException("ZookeeperDistributedLock`s tryLock method is unsupported");
  98. }
  99. @Override
  100. public boolean tryLock(long time, @NotNull TimeUnit unit) {
  101. throw new UnsupportedOperationException("ZookeeperDistributedLock`s tryLock method is unsupported");
  102. }
  103. @Override
  104. public void unlock() {
  105. // -1 代表不使用乐观锁删除,类似 delete --force
  106. Integer lockTimes = LOCK_TIMES.get();
  107. if (lockTimes != null && lockTimes > 0) {
  108. // 计算剩下的重入次数
  109. int leftLockTime = lockTimes - 1;
  110. LOCK_TIMES.set(leftLockTime);
  111. // 剩下为 0 的时候,代表完全解锁,需要删除临时节点
  112. if (leftLockTime == 0) {
  113. try {
  114. this.zooKeeper.delete(lockNodePath, -1);
  115. LOCK_TIMES.remove();
  116. } catch (InterruptedException | KeeperException e) {
  117. throw new RuntimeException(e);
  118. }
  119. }
  120. }
  121. }
  122. /**
  123. * 根据当前节点,获取前一个节点
  124. *
  125. * @param currentNodePath 当前节点路径
  126. * @return 前一个节点
  127. */
  128. private String getPreviousNodePath(String currentNodePath) {
  129. try {
  130. // 截取当前临时节点编号
  131. long currentNodeSeq = Long.parseLong(StrUtil.subAfter(currentNodePath, "-", true));
  132. // 获取锁根路径下的所有临时节点
  133. List<String> nodes = this.zooKeeper.getChildren(lockRootPath, false);
  134. if (CollUtil.isEmpty(nodes)) {
  135. return null;
  136. }
  137. // 用于标记遍历所有子节点时,离当前最近的一次编号
  138. long previousNodeSeq = 0L;
  139. String previousNodePath = null;
  140. for (String nodePath : nodes) {
  141. // 获取每个临时节点编号
  142. long nodeSeq = Long.parseLong(StrUtil.subAfter(nodePath, "-", true));
  143. if (nodeSeq < currentNodeSeq && nodeSeq > previousNodeSeq) {
  144. previousNodeSeq = nodeSeq;
  145. previousNodePath = nodePath;
  146. }
  147. }
  148. return previousNodePath;
  149. } catch (KeeperException | InterruptedException e) {
  150. throw new RuntimeException(e);
  151. }
  152. }
  153. }
  154. ```
  155. #### 1.2.2.ZkClient
  156. > 配置 Zookeeper 客户端连接
  157. ```java
  158. @Slf4j
  159. public class ZkClient {
  160. /**
  161. * 连接地址
  162. */
  163. @Getter
  164. private final String connectServer;
  165. /**
  166. * 节点根路径,默认是 /locks
  167. */
  168. @Getter
  169. private final String lockRootPath;
  170. @Getter
  171. private ZooKeeper zooKeeper;
  172. private static final String DEFAULT_ROOT_PATH = "/locks";
  173. public ZkClient(String connectServer) {
  174. this.connectServer = connectServer;
  175. this.lockRootPath = DEFAULT_ROOT_PATH;
  176. }
  177. public ZkClient(String connectServer, String lockRootPath) {
  178. this.connectServer = connectServer;
  179. this.lockRootPath = lockRootPath;
  180. }
  181. public void init() {
  182. try {
  183. this.zooKeeper = new ZooKeeper(connectServer, 3000, new Watcher() {
  184. @Override
  185. public void process(WatchedEvent watchedEvent) {
  186. if (Event.KeeperState.SyncConnected == watchedEvent.getState() && Event.EventType.None == watchedEvent.getType()) {
  187. log.info("===========> zookeeper connected <===========");
  188. }
  189. }
  190. });
  191. // 判断根节点是否存在,不存在则创建
  192. if (this.zooKeeper.exists(lockRootPath, false) == null) {
  193. this.zooKeeper.create(lockRootPath, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
  194. }
  195. } catch (IOException | KeeperException | InterruptedException e) {
  196. log.error("===========> zookeeper connect error <===========");
  197. throw new RuntimeException(e);
  198. }
  199. }
  200. public void destroy() {
  201. if (this.zooKeeper != null) {
  202. try {
  203. zooKeeper.close();
  204. log.info("===========> zookeeper disconnected <===========");
  205. } catch (InterruptedException e) {
  206. log.error("===========> zookeeper disconnect error <===========");
  207. throw new RuntimeException(e);
  208. }
  209. }
  210. }
  211. }
  212. ```
  213. #### 1.2.3.ZookeeperDistributedLockClient
  214. > 获取一把 Zookeeper 分布式锁
  215. ```java
  216. @RequiredArgsConstructor
  217. public class ZookeeperDistributedLockClient implements DistributedLockClient {
  218. private final ZkClient zkClient;
  219. /**
  220. * 获取一把锁
  221. *
  222. * @param lockKey 锁的标识
  223. * @param lockTime 锁的时间
  224. * @param timeUnit 锁的时间单位
  225. * @return 锁
  226. */
  227. @Override
  228. public DistributedLock getLock(String lockKey, long lockTime, TimeUnit timeUnit) {
  229. return new ZookeeperDistributedLock(zkClient, lockKey, lockTime, timeUnit);
  230. }
  231. }
  232. ```
  233. #### 1.2.4.自动装配
  234. > 替换 `demo-distributed-lock-api` 中的默认实现
  235. ```java
  236. @Configuration(proxyBeanMethods = false)
  237. public class ZookeeperDistributedLockAutoConfiguration {
  238. @Bean(initMethod = "init", destroyMethod = "destroy")
  239. public ZkClient zkClient() {
  240. return new ZkClient("127.0.0.1:2181");
  241. }
  242. @Bean
  243. public ZookeeperDistributedLockClient distributedLockClient(ZkClient zkClient) {
  244. return new ZookeeperDistributedLockClient(zkClient);
  245. }
  246. }
  247. ```
  248. ## 2.测试
  249. ### 2.1.环境搭建
  250. 主要是 mysql 及 zookeeper 环境的搭建,这里我提供了 docker-compose 文件,方便同学们一键启动测试环境
  251. ```bash
  252. $ cd demo-distributed-lock/demo-distributed-lock-zookeeper
  253. $ docker compose -f docker-compose.env.yml up
  254. ```
  255. ### 2.2.测试流程
  256. 这里我通过 Apache Bench 进行模拟并发场景,我也构建了一个压测镜像 `xkcoding/ab:alpine-3.16.2` ,方便同学们进行快速测试
  257. > 注意:每次启动项目,都会在重置库存,你也可以手动调用 `/demo/stock/reset` 接口重置
  258. #### 2.2.1.测试无分布式锁下高并发请求是否会发生超卖
  259. 1. 把 `ZookeeperDistributedLockAutoConfiguration` 类全部注释掉,这将不会装配 Zookeeper 分布式锁
  260. 2. 启动 `ZookeeperDistributedLockApplication`
  261. 3. 模拟 5000 请求数 100 并发的压测环境 `docker run -it --rm xkcoding/ab:alpine-3.16.2 ab -n 5000 -c 100 http://${替换成你电脑的内网IP}:8080/demo/stock/reduce`
  262. 4. 等待压测结束,前往数据库查看库存是否从 5000 减为 0
  263. #### 2.2.2.测试 Zookeeper 分布式锁
  264. 1. 把 `ZookeeperDistributedLockAutoConfiguration` 类的注释解开,此时 Spring Boot 会自动装配我们的 Zookeeper 分布式锁
  265. 2. 再次启动 `ZookeeperDistributedLockApplication`
  266. 3. 再次模拟 5000 请求数 100 并发的压测环境 `docker run -it --rm xkcoding/ab:alpine-3.16.2 ab -n 5000 -c 100 http://${替换成你电脑的内网IP}:8080/demo/stock/reduce`
  267. 4. 等待压测结束,前往数据库查看库存是否从 5000 减为 0
  268. ## 3.参考
  269. - [Zookeeper官方 API 文档](https://zookeeper.apache.org/doc/r3.8.0/zookeeperProgrammers.html)