分布式锁是控制分布式系统之间同步访问共享资源的一种方式。
下面介绍 zookeeper 如何实现分布式锁,讲解排他锁和共享锁两类分布式锁。

排他锁

排他锁(Exclusive Locks),又被称为写锁或独占锁,如果事务T1对数据对象O1加上排他锁,那么整个加锁期间,只允许事务T1对O1进行读取和更新操作,其他任何事务都不能进行读或写。
定义锁:

  1. /exclusive_lock/lock

实现方式:
利用 zookeeper 的同级节点的唯一性特性,在需要获取排他锁时,所有的客户端试图通过调用 create() 接口,在 /exclusive_lock 节点下创建临时子节点 /exclusive_lock/lock,最终只有一个客户端能创建成功,那么此客户端就获得了分布式锁。同时,所有没有获取到锁的客户端可以在 /exclusive_lock 节点上注册一个子节点变更的 watcher 监听事件,以便重新争取获得锁。

1.Lock接口

  1. public interface ZkLock {
  2. boolean lock();
  3. void unlock();
  4. }

2.分布式排他锁实现

  1. /**
  2. * 分布式排他锁
  3. * 支持重入
  4. */
  5. public class MutexLock implements ZkLock {
  6. private static final String MUTEX_LOCK_ROOT = "/mutex-lock-root";
  7. //创建的节点path
  8. private final String lockName;
  9. // 存放线程重入锁
  10. private final ConcurrentMap<Thread, LockData> threadLockDataMap = Maps.newConcurrentMap();
  11. public MutexLock(String lockName) {
  12. this.lockName = PathUtils.validatePath(MUTEX_LOCK_ROOT + "-" + lockName);
  13. }
  14. //线程锁的数据
  15. private static class LockData {
  16. final Thread owningThread;
  17. final AtomicInteger lockCount;
  18. private LockData(Thread owningThread) {
  19. this.owningThread = owningThread;
  20. lockCount = new AtomicInteger(1);
  21. }
  22. }
  23. @Override
  24. public boolean lock() {
  25. boolean result = true;
  26. Thread currentThread = Thread.currentThread();
  27. //获取线程锁
  28. LockData lockData = threadLockDataMap.get(currentThread);
  29. if (null != lockData) {
  30. //重入次数加一
  31. int i = lockData.lockCount.incrementAndGet();
  32. TestLock.printLog(currentThread.getName(), "加锁 ,重入次数", i);
  33. return result;
  34. }
  35. try {
  36. //创建zookeeper锁
  37. String lockPath = innerLock(currentThread.getName());
  38. if (null != lockPath) {
  39. lockData = new LockData(currentThread);
  40. //将当前线程放入map中存放
  41. TestLock.printLog(currentThread.getName(), "加锁 ,重入次数", 1);
  42. threadLockDataMap.put(currentThread, lockData);
  43. }
  44. } catch (Exception e) {
  45. e.printStackTrace();
  46. result = false;
  47. }
  48. return result;
  49. }
  50. @Override
  51. public void unlock() {
  52. Thread currentThread = Thread.currentThread();
  53. //获取锁数据
  54. LockData lockData = threadLockDataMap.get(currentThread);
  55. if (null == lockData) {
  56. return;
  57. }
  58. //重入次数减一
  59. int count = lockData.lockCount.decrementAndGet();
  60. TestLock.printLog(currentThread.getName(), "解锁 ,重入次数", count);
  61. //重入次数依然大于0,返回等待继续解锁
  62. if (0 < count) {
  63. return;
  64. }
  65. //重入次数减为0,移除zookeeper节点,解锁
  66. threadLockDataMap.remove(currentThread);
  67. //允许重试次
  68. ZooKeeper zooKeeper = LockUtils.newZookeeper();
  69. int retry = 5;
  70. while (retry-- > 0) {
  71. try {
  72. zooKeeper.delete(lockName, -1);
  73. retry = 0;
  74. } catch (InterruptedException | KeeperException e) {
  75. e.printStackTrace();
  76. }
  77. }
  78. }
  79. private String innerLock(String threadName) throws Exception {
  80. ZooKeeper zooKeeper = LockUtils.newZookeeper();
  81. CountDownLatch countDownLatch = new CountDownLatch(1);
  82. int i = 0;
  83. TestLock.printLog(threadName, "尝试上锁 ,次数", ++i);
  84. //创建zookeeper节点
  85. String result = createPath(zooKeeper, countDownLatch, threadName, i);
  86. //await等待上锁成功,或者锁释放
  87. countDownLatch.await();
  88. //如果上锁失败,会一直尝试上锁
  89. while (result == null) {
  90. TestLock.printLog(threadName, "尝试上锁 ,次数", ++i);
  91. result = createPath(zooKeeper, countDownLatch, threadName, i);
  92. // countDownLatch.await();
  93. }
  94. return result;
  95. }
  96. private String createPath(ZooKeeper zooKeeper, CountDownLatch countDownLatch, String threadName, int count) {
  97. String resultPath = null;
  98. try {
  99. // 创建临时节点
  100. resultPath = zooKeeper.create(lockName, "".getBytes(StandardCharsets.UTF_8), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
  101. TestLock.printLog(threadName, "竞争锁", "成功");
  102. countDownLatch.countDown();
  103. } catch (KeeperException.NodeExistsException e) {
  104. // 节点存在(锁获取失败),创建监听节点状态
  105. TestLock.printLog(threadName, "竞争锁", "失败, 上锁次数" + count);
  106. watchPath(zooKeeper, countDownLatch, threadName, count);
  107. } catch (Exception e) {
  108. e.printStackTrace();
  109. }
  110. return resultPath;
  111. }
  112. private void watchPath(ZooKeeper zooKeeper, CountDownLatch countDownLatch, String threadName, int count) {
  113. try {
  114. Stat exists = zooKeeper.exists(lockName, event -> {
  115. if (Watcher.Event.EventType.NodeDeleted == event.getType()) {
  116. TestLock.printLog(threadName, "监听锁释放", "加入竞争, 序次 : " + count);
  117. countDownLatch.countDown();
  118. }
  119. });
  120. if (null == exists) {
  121. //节点不存在
  122. countDownLatch.countDown();
  123. }
  124. } catch (KeeperException | InterruptedException e) {
  125. e.printStackTrace();
  126. }
  127. }

3.单例zookeeper连接类

  1. public class LockUtils {
  2. private static ZooKeeper zooKeeper;
  3. private LockUtils() {
  4. }
  5. public static ZooKeeper newZookeeper() {
  6. if (null == zooKeeper) {
  7. init();
  8. }
  9. return zooKeeper;
  10. }
  11. private static void init() {
  12. if (null != zooKeeper) {
  13. return;
  14. }
  15. synchronized (LockUtils.class) {
  16. if (null != zooKeeper) {
  17. return;
  18. }
  19. try {
  20. CountDownLatch countDownLatch = new CountDownLatch(1);
  21. zooKeeper = new ZooKeeper("localhost:2181", 5000, event -> {
  22. if (Watcher.Event.KeeperState.SyncConnected == event.getState()) {
  23. countDownLatch.countDown();
  24. }
  25. });
  26. countDownLatch.await();
  27. System.out.println("===zookeeper init success===");
  28. } catch (Exception e) {
  29. e.printStackTrace();
  30. }
  31. }
  32. }
  33. }

4.多线程测试

  1. public class TestLock {
  2. public static void printLog(String threadName, Object tag, Object avg) {
  3. SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss SSS");
  4. String date = dateFormat.format(new Date());
  5. System.out.println(date + ": 线程:" + threadName + " ," + tag + " : " + avg);
  6. }
  7. public static void main(String[] args) {
  8. final ZkLock lock = new MutexLock("Test");
  9. CountDownLatch countDownLatch = new CountDownLatch(1);
  10. for (int i = 0; i < 3; i++) {
  11. int finalI = i;
  12. new Thread(() -> {
  13. try {
  14. printLog("Thread " + finalI , "Waiting", "已就绪");
  15. countDownLatch.await();
  16. if (lock.lock()) {
  17. Thread.sleep(10);
  18. if (lock.lock()) {
  19. Thread.sleep(20);
  20. lock.unlock();
  21. }
  22. lock.unlock();
  23. }
  24. } catch (InterruptedException e) {
  25. e.printStackTrace();
  26. }
  27. }).start();
  28. }
  29. countDownLatch.countDown();
  30. printLog("Main", "", "已就绪");
  31. }

5.观察结果

  1. 17:57:02 615: 线程:Thread 0 ,Waiting : 已就绪
  2. 17:57:02 615: 线程:Main , : 已就绪
  3. 17:57:02 615: 线程:Thread 1 ,Waiting : 已就绪
  4. 17:57:02 615: 线程:Thread 2 ,Waiting : 已就绪
  5. ===zookeeper init success===
  6. 17:57:03 580: 线程:Thread-2 ,尝试上锁 ,次数 : 1
  7. 17:57:03 580: 线程:Thread-1 ,尝试上锁 ,次数 : 1
  8. 17:57:03 580: 线程:Thread-0 ,尝试上锁 ,次数 : 1
  9. 17:57:03 602: 线程:Thread-2 ,竞争锁 : 成功
  10. 17:57:03 603: 线程:Thread-2 ,加锁 ,重入次数 : 1
  11. 17:57:03 614: 线程:Thread-2 ,加锁 ,重入次数 : 2
  12. 17:57:03 616: 线程:Thread-0 ,竞争锁 : 失败, 上锁次数1
  13. 17:57:03 616: 线程:Thread-1 ,竞争锁 : 失败, 上锁次数1
  14. 17:57:03 646: 线程:Thread-2 ,解锁 ,重入次数 : 1
  15. 17:57:03 646: 线程:Thread-2 ,解锁 ,重入次数 : 0
  16. 17:57:03 653: 线程:Thread-1 ,监听锁释放 : 加入竞争, 序次 : 1
  17. 17:57:03 653: 线程:Thread-0 ,监听锁释放 : 加入竞争, 序次 : 1
  18. 17:57:03 653: 线程:Thread-1 ,尝试上锁 ,次数 : 2
  19. 17:57:03 654: 线程:Thread-0 ,尝试上锁 ,次数 : 2
  20. 17:57:03 657: 线程:Thread-1 ,竞争锁 : 成功
  21. 17:57:03 658: 线程:Thread-1 ,加锁 ,重入次数 : 1
  22. 17:57:03 659: 线程:Thread-0 ,竞争锁 : 失败, 上锁次数2
  23. 17:57:03 661: 线程:Thread-0 ,尝试上锁 ,次数 : 3
  24. 17:57:03 666: 线程:Thread-0 ,竞争锁 : 失败, 上锁次数3
  25. 17:57:03 668: 线程:Thread-0 ,尝试上锁 ,次数 : 4
  26. 17:57:03 671: 线程:Thread-0 ,竞争锁 : 失败, 上锁次数4
  27. 17:57:03 673: 线程:Thread-0 ,尝试上锁 ,次数 : 5
  28. 17:57:03 676: 线程:Thread-0 ,竞争锁 : 失败, 上锁次数5
  29. 17:57:03 678: 线程:Thread-1 ,加锁 ,重入次数 : 2
  30. 17:57:03 678: 线程:Thread-0 ,尝试上锁 ,次数 : 6
  31. 17:57:03 682: 线程:Thread-0 ,竞争锁 : 失败, 上锁次数6
  32. 17:57:03 685: 线程:Thread-0 ,尝试上锁 ,次数 : 7
  33. 17:57:03 688: 线程:Thread-0 ,竞争锁 : 失败, 上锁次数7
  34. 17:57:03 690: 线程:Thread-0 ,尝试上锁 ,次数 : 8
  35. 17:57:03 694: 线程:Thread-0 ,竞争锁 : 失败, 上锁次数8
  36. 17:57:03 696: 线程:Thread-0 ,尝试上锁 ,次数 : 9
  37. 17:57:03 700: 线程:Thread-0 ,竞争锁 : 失败, 上锁次数9
  38. 17:57:03 702: 线程:Thread-0 ,尝试上锁 ,次数 : 10
  39. 17:57:03 705: 线程:Thread-0 ,竞争锁 : 失败, 上锁次数10
  40. 17:57:03 708: 线程:Thread-0 ,尝试上锁 ,次数 : 11
  41. 17:57:03 710: 线程:Thread-1 ,解锁 ,重入次数 : 1
  42. 17:57:03 710: 线程:Thread-1 ,解锁 ,重入次数 : 0
  43. 17:57:03 712: 线程:Thread-0 ,竞争锁 : 失败, 上锁次数11
  44. 17:57:03 714: 线程:Thread-0 ,监听锁释放 : 加入竞争, 序次 : 4
  45. 17:57:03 714: 线程:Thread-0 ,监听锁释放 : 加入竞争, 序次 : 6
  46. 17:57:03 714: 线程:Thread-0 ,监听锁释放 : 加入竞争, 序次 : 7
  47. 17:57:03 715: 线程:Thread-0 ,监听锁释放 : 加入竞争, 序次 : 9
  48. 17:57:03 715: 线程:Thread-0 ,监听锁释放 : 加入竞争, 序次 : 3
  49. 17:57:03 715: 线程:Thread-0 ,监听锁释放 : 加入竞争, 序次 : 8
  50. 17:57:03 715: 线程:Thread-0 ,尝试上锁 ,次数 : 12
  51. 17:57:03 715: 线程:Thread-0 ,监听锁释放 : 加入竞争, 序次 : 2
  52. 17:57:03 716: 线程:Thread-0 ,监听锁释放 : 加入竞争, 序次 : 10
  53. 17:57:03 716: 线程:Thread-0 ,监听锁释放 : 加入竞争, 序次 : 5
  54. 17:57:03 719: 线程:Thread-0 ,竞争锁 : 成功
  55. 17:57:03 719: 线程:Thread-0 ,加锁 ,重入次数 : 1
  56. 17:57:03 741: 线程:Thread-0 ,加锁 ,重入次数 : 2
  57. 17:57:03 772: 线程:Thread-0 ,解锁 ,重入次数 : 1
  58. 17:57:03 772: 线程:Thread-0 ,解锁 ,重入次数 : 0

共享锁

共享锁(Shared Locks),又称读锁。如果事务T1对数据对象O1加上了共享锁,那么当前事务只能对O1进行读取操作,其他事务也只能对这个数据对象加共享锁,直到该数据对象上的所有共享锁都释放。
定义锁:

/shared_lock/[hostname]-请求类型W/R-序号

实现方式:
1、客户端调用 create 方法创建类似定义锁方式的临时顺序节点。
Zookeeper 分布式锁实现原理 - 图1
2、客户端调用 getChildren 接口来获取所有已创建的子节点列表。
3、判断是否获得锁,对于读请求如果所有比自己小的子节点都是读请求或者没有比自己序号小的子节点,表明已经成功获取共享锁,同时开始执行度逻辑。对于写请求,如果自己不是序号最小的子节点,那么就进入等待。
4、如果没有获取到共享锁,读请求向比自己序号小的最后一个写请求节点注册 watcher 监听,写请求向比自己序号小的最后一个节点注册watcher 监听。
实际开发过程中,可以 curator 工具包封装的API帮助我们实现分布式锁。

<dependency>
  <groupId>org.apache.curator</groupId>
  <artifactId>curator-recipes</artifactId>
  <version>x.x.x</version>
</dependency>

curator 的几种锁方案 :

  • 1、InterProcessMutex:分布式可重入排它锁
  • 2、InterProcessSemaphoreMutex:分布式排它锁
  • 3、InterProcessReadWriteLock:分布式读写锁

下面例子模拟 50 个线程使用重入排它锁 InterProcessMutex 同时争抢锁:

public class InterprocessLock {
    public static void main(String[] args)  {
        CuratorFramework zkClient = getZkClient();
        String lockPath = "/lock";
        InterProcessMutex lock = new InterProcessMutex(zkClient, lockPath);
        //模拟50个线程抢锁
        for (int i = 0; i < 50; i++) {
            new Thread(new TestThread(i, lock)).start();
        }
    }


    static class TestThread implements Runnable {
        private Integer threadFlag;
        private InterProcessMutex lock;

        public TestThread(Integer threadFlag, InterProcessMutex lock) {
            this.threadFlag = threadFlag;
            this.lock = lock;
        }

        @Override
        public void run() {
            try {
                lock.acquire();
                System.out.println("第"+threadFlag+"线程获取到了锁");
                //等到1秒后释放锁
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                try {
                    lock.release();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private static CuratorFramework getZkClient() {
        String zkServerAddress = "192.168.3.39:2181";
        ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, 3, 5000);
        CuratorFramework zkClient = CuratorFrameworkFactory.builder()
            .connectString(zkServerAddress)
            .sessionTimeoutMs(5000)
            .connectionTimeoutMs(5000)
            .retryPolicy(retryPolicy)
            .build();
        zkClient.start();
        return zkClient;
    }
}

控制台每间隔一秒钟输出一条记录:
Zookeeper 分布式锁实现原理 - 图2
下载源码