首页 > 技术文章 > Apache Curator之InterProcessMutex抢购案例(三)

shileibrave 2018-10-26 10:30 原文

 

上一节讲了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个线程竞争锁。关于超时删除临时节点我们下一节分析源码再说。

 

推荐阅读