上一节讲了Apache Curator之分布式锁原理(二),在分析InterProcessMutex源码之前,我们先通过一个简单的手机抢购案例更深入理解分布式锁的原理。废话不多说,先上代码:
手机实体Bean类Phone.java:很简单,只有一个number字段,模拟手机库存数量。

1 package com.youguu.skill;
2
3 public class Phone {
4 /**
5 * 商品库存,默认有5部手机
6 */
7 private static int number = 5;
8
9 public static int getNumber() {
10 return number;
11 }
12
13 public static void setNumber(int number) {
14 Phone.number = number;
15 }
16
17 }

用户类:

1 package com.youguu.skill;
2
3 import org.apache.curator.RetryPolicy;
4 import org.apache.curator.framework.CuratorFramework;
5 import org.apache.curator.framework.CuratorFrameworkFactory;
6 import org.apache.curator.framework.recipes.locks.InterProcessMutex;
7 import org.apache.curator.retry.ExponentialBackoffRetry;
8
9 public class User {
10
11 public static void main(String[] args) {
12 //重试策略, 参数1:等待时间, 参数2:重试次数
13 RetryPolicy policy = new ExponentialBackoffRetry(2000, 3);
14
15 //创建zookeeper客户端连接
16 CuratorFramework client = CuratorFrameworkFactory.builder().connectString("192.168.1.30:2181").retryPolicy(policy).build();
17 client.start();
18
19 //获取锁对象
20 final InterProcessMutex mutex = new InterProcessMutex(client, "/locks");
21
22 //创建10个线程,相当于10个用户去抢购5部手机
23 for (int i = 0; i < 10; i++) {
24 new Thread(() -> {
25 try {
26 //请求锁
27 mutex.acquire();
28
29 //执行抢购业务
30 buy();
31 } catch (Exception e) {
32 e.printStackTrace();
33 } finally {
34 try {
35 //释放锁
36 mutex.release();
37 } catch (Exception e) {
38 e.printStackTrace();
39 }
40 }
41 }).start();
42 }
43 }
44
45 /**
46 * 抢购
47 */
48 public static void buy() {
49 System.out.println("【" + Thread.currentThread().getName() + "】开始抢购");
50 //获取剩余手机数量
51 int currentNumber = Phone.getNumber();
52
53 if (currentNumber == 0) {
54 System.out.println("抢购已结束,下次再来吧");
55 } else {
56 System.out.println("剩余手机数量:" + currentNumber);
57
58 //睡眠3秒,模拟业务逻辑处理耗时时间
59 try {
60 Thread.sleep(3000);
61 } catch (InterruptedException e) {
62 e.printStackTrace();
63 }
64
65 //购买后数量减1
66 currentNumber--;
67 Phone.setNumber(currentNumber);
68 }
69 System.out.println("【" + Thread.currentThread().getName() + "】 购买结束");
70 System.out.println("-----------------------------------------");
71 }
72 }

13行:配置重试策略
16,17行:获取zookeeper连接
20行:获取锁对象,这里相当于用户只是获得了锁对象的引用,没有执行加锁动作
23行:创建10个线程,相当于10个用户去抢购5部手机,最后肯定是5个用户抢到手机,5个用户没有抢到。
27行:尝试获得锁
30行:获得锁成功,执行业务逻辑(这里只是对库存字段number减一)
36行:释放锁,注意是在finally块里执行的。
我们观察一下输入日志:

【Thread-2】开始抢购 剩余手机数量:5 【Thread-2】 购买结束 ----------------------------------------- 【Thread-6】开始抢购 剩余手机数量:4 【Thread-6】 购买结束 ----------------------------------------- 【Thread-1】开始抢购 剩余手机数量:3 【Thread-1】 购买结束 ----------------------------------------- 【Thread-3】开始抢购 剩余手机数量:2 【Thread-3】 购买结束 ----------------------------------------- 【Thread-8】开始抢购 剩余手机数量:1 【Thread-8】 购买结束 ----------------------------------------- 【Thread-4】开始抢购 抢购已结束,下次再来吧 【Thread-4】 购买结束 ----------------------------------------- 【Thread-10】开始抢购 抢购已结束,下次再来吧 【Thread-10】 购买结束 ----------------------------------------- 【Thread-7】开始抢购 抢购已结束,下次再来吧 【Thread-7】 购买结束 ----------------------------------------- 【Thread-9】开始抢购 抢购已结束,下次再来吧 【Thread-9】 购买结束 ----------------------------------------- 【Thread-5】开始抢购 抢购已结束,下次再来吧 【Thread-5】 购买结束 -----------------------------------------

从线程的名字可以看到,每个线程获得锁后,其他线程都是处于等待状态,直到当前线程释放锁,其它线程才能继续执行。
从后面5个线程的输出可以看到,最后5个线程(用户)都没有抢到手机。
所以整个输出是符合我们的预期的。
现在我们把执行镜头放慢,看看zookeeper节点在这个过程中是怎么变化的。
在User类30行打一个断点,此时观察zookeeper节点如下图:

共10个path,也验证了之前所说的,每个线程在获得锁之前都会事先把临时顺序路径创建好。
依然保持住在User类30行打的断点,我们可以观察到,已执行线程数和zookeeper剩余path数量满足如下关系:
| 已执行线程数量 | 剩余zk path数量 |
| 0 | 10 |
| 1 | 9 |
| 2 | 8 |
| 3 | 7 |
| 4 | 6 |
| 5 | 5 |
| 6 | 4 |
| 7 | 3 |
| 8 | 2 |
| 9 | 1 |
| 10 | 0 |
这也充分说明了,每一个线程释放锁后会删除path节点。
观察到这里你们以为就完了?还没有,如果断点一直卡在30行,隔了一段时间我们再刷新zookeeper节点,发现locks目录下一个节点也没有了。

但是按一下F9,再刷新zookeeper节点,发现locks目录下又有临时节点了。

在这个图里我已经按了两下F9。可以看到还有8个节点,也就是说剩余8个线程竞争锁。关于超时删除临时节点我们下一节分析源码再说。
本文通过手机抢购场景,深入解析Apache Curator分布式锁的实现原理。利用ZooKeeper作为协调服务,演示了10个线程如何公平地竞争资源,确保了在并发环境下资源的一致性和安全性。
2548

被折叠的 条评论
为什么被折叠?



