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