zookeeper 分布式锁实现(附源码及方案测试结果)

一、 Zookeeper

项目地址:https://github.com/bao17634/zookeeper_lock.git

1.1 zookeeper简介:

  • zookeeper是一个分布、开源的应用程序协调服务,是集群的管理者,监视着集群中各个节点的状态,根据节点的反馈进行下一步合理操作。主要解决的是分布式应用经常遇到的数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。
  • zookeeper组成:


    zookeeper组成图

1.2 zookeeper功能:

提供文件系统及通知机制。

1.2.1 文件系统:
zookeeper维护一个类似文件系统的数结构,如下图:

结构图

每子目录如NameService都被作为zknode,和文件一样,可以自由增加及删除,唯一不同的是他可以用来存储数据。zknode分为四种类型:

    1. PERSISTENT: 持久化目录节点。(客户端与zookeeper断开连接后,该节点依旧存在)。
    1. PERSISTENT_SEQUENTIAL: 持久化顺序编号目录节点。(客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号)
    1. EPHEMERAL: 临时目录节点(客户端与zookeeper断开连接后,该节点被删除)
    1. EPHEMERAL_SEQUENTIAL: 临时顺序编号目录节点。(客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号)

1.2.2 通知机制: 客户端注册监听他关心的目录节点,当目录节点发生变化(数据改变、被删除、目录节点增加删除)时,zookeeper会通知客户端。
集群架构如图:

二、 zookeeper集群

2.1 zookeeper集群搭建教程: https://www.jianshu.com/p/a5fda39f20d0

2.2 集群简介:

为了保证高可用,zookeeper需要以集群形态来部署,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么zookeeper本身仍然是可用的。客户端在使用zookeeper时,需要知道集群机器列表,通过与集群中的某一台机器建立TCP连接来使用服务,客户端使用这个TCP链接来发送请求、获取结果、获取监听事件以及发送心跳包。如果这个连接异常断开了,客户端可以连接到另外的机器上。这样客户端的读请求就可以被任意一台机器处理,如果请求在节点上注册了监听器,这个监听器也是由所连接的zookeeper机器来处理。

集群架构如下图:

集群架构图

说明: 对于写请求,这些请求会同时发给其他zookeeper机器并且达成一致后,请求才会返回成功,因此随着zookeeper的集群增多,读请求的吞吐量提高但是写请求的吞吐量会下降。

2.3 zookeeper分布式锁实现

2.3.1 实现流程:

  • 首先在zookeeper创建根节点,也就是持久节点(rootNode)。

  • 在根节点(rootNode)下创建临时节点(node_n):

    1) 如果当前创建的节点的序号是最小话,就认定该客户端已经获得锁。

    2)如果不是最小的节点,说明获取锁失败,此时客户端就要找比自己小的节点,对其注册事件监听器。

  • 获取锁的客户端在执行完业务就删除当前最小节点。

  • 删除最小节点后客户端会通过监听器收到通知,然后再次判断是否自己的节点最小,是的话进行加锁,不是的话重复上述操作。

  • 加锁流程图:

zookeeper组成图
zookeeper结构图

2.3.2 分布式锁代码实现(利用curator实现):

1)引入依赖:

    
     org.apache.curator
     curator-framework
     4.0.0
   
   
     org.apache.curator
     curator-recipes
     4.0.0
   

2)编写配置类:

@Configuration
public class CuratorConfiguration {
    @Autowired
    ConfigProperties configProperties;


    @Bean(initMethod = "start")
    public CuratorFramework curatorFramework() {
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(configProperties.getElapsedTimeMs(), configProperties.getRetryCount());
        return CuratorFrameworkFactory.newClient(
                configProperties.getConnectString(),
                configProperties.getSessionTimeoutMs(),
                configProperties.getConnectionTimeoutMs(),
                retryPolicy);
    }
}
  public static CuratorFramework newClient(String connectString, int sessionTimeoutMs, int connectionTimeoutMs, RetryPolicy retryPolicy)

创建zk连接,该方法要求传入的三个参数分别是:ip:端口、会话超时时间、连接超时时间、重试策略(由public ExponentialBackoffRetry(int baseSleepTimeMs, int maxRetries)方法提供,其要求传入的参数粉分别是: 重试间隔时间、重试次数)

4)curator的分布式锁:

curator提供了四种分布式锁,分别是:

image
  • InterProcessMutex:分布式可重入排它锁
  • InterProcessSemaphoreMutex:分布式排它锁
  • InterProcessReadWriteLock:分布式读写锁
  • InterProcessMultiLock:将多个锁作为单个实体管理的容器

1> 分布式可重入排它锁实现:
代码:

    public ApiResult processMutex() throws Exception {
        log.info("第{}个线程请求锁", ++count);
        try {
            // 创建分布式可重入排他锁,根节点为ROOT_LOCK_NODE
            InterProcessMutex mutex = new InterProcessMutex(curatorFramework, ROOT_LOCK_NODE );
            //加锁
            if (mutex.acquire(TIME_OUT, TimeUnit.SECONDS)) {
                try {
                    log.info("获得锁线程为:{}", ++number);
                    return ApiResult.ok("加锁成功");
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    log.info("解锁");
                    //释放锁
                    mutex.release();
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        throw new RuntimeException("拿到锁失败");
    }

2> 分布式排它锁实现:
代码:

public ApiResult semaphoreMutex() throws Exception {
        log.info("第{}个线程请求锁", ++count);
        try {
             // 创建分布式排他锁,根节点为ROOT_LOCK_NODE
            InterProcessSemaphoreMutex mutex = new InterProcessSemaphoreMutex(curatorFramework, ROOT_LOCK_NODE);
            //加锁
            if (mutex.acquire(TIME_OUT, TimeUnit.SECONDS)) {
                try {
                    log.info("获得锁线程为:{}", ++number);
                    return ApiResult.ok("加锁成功");
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                   //解锁
                    mutex.release();
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        throw new RuntimeException("拿到锁失败");
    }

2> 分布式读写锁实现:
代码:

public ApiResult readWriteLock() throws Exception {
        log.info("第{}个线程请求锁", ++count);
        try {
             // 创建分布式读写锁,根节点为ROOT_LOCK_NODE
            InterProcessReadWriteLock readWriteLock = new InterProcessReadWriteLock(curatorFramework, ROOT_LOCK_NODE);
            //创建读锁
            InterProcessMutex readLock = readWriteLock.readLock();
            //创建写锁
            InterProcessMutex writeLock = readWriteLock.writeLock();
            try {
                //注意只有先得到写锁在得到读锁,不能反过来
                if (!writeLock.acquire(TIME_OUT, TimeUnit.SECONDS)) {
                    throw new RuntimeException("得到写锁失败");
                }
                log.info("已经得到写锁");
                if (!readLock.acquire(TIME_OUT, TimeUnit.SECONDS)) {
                    throw new RuntimeException("得到读锁失败");
                }
                log.info("已经得到读锁");
                log.info("获得锁线程数为:{}", ++number);
                return ApiResult.ok("加锁成功");
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
               //解锁
                writeLock.release();
                readLock.release();
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

三、分布式锁测试情况:

3.1 测试环境:

  • 系统:centoOs
  • 软件:jemeter
  • 测试压力:1 x 10000、100 x 100、200 x 100、500 x 100
  • 说明:测试压力=线程数 x 每个线程循环数

3.2 可重入排它锁:

1 x 10000压力测试
100 x 100压力测试
200 x 100压力测试
500 x 100压力测试

3.3 排他锁:

1 x 10000压力测试
100 x 100压力测试
200 x 100压力测试
500 x 100压力测试

3.4 读写锁:

1 x 10000 压力测试
100 x 100 测试压力
200 x 100 测试压力
500 x 100 测试压力

四、总结:

本文主要介绍了使用curator来实现zookeeper分布式锁,在研究zookeeper过程中我也写了利用zookeeper原生API来实现分布式锁,但是实现过程比较复杂,在测试的时候单线程虽然性能与上面三种性能差不多,但是在多线程测试中性能就非常差,所以这里就没有重点去介绍,具体实现请移步我的github。

测试结果分析(主要考虑多线程):

  • 性能指数: 可重入排它锁(2.992) > 排它锁(2.98) > 读写锁(2.974)
  • 执行时间: 可重入排它锁(995) < 读写锁(1638.93) < 排它锁(1477)
  • 请求失败率 可重入排它锁(0)=排它锁(0)< 读写锁(0.28%)
  • 综合分析: 从上面的测试结果来看三者个方案在低并发下性能差距不大,但是从高并发测试结果来看可重入排它锁很明显优于其他两种方案,所以从整体上性能上看:可重入排它锁 > 排它锁 > 读写锁

你可能感兴趣的:(zookeeper 分布式锁实现(附源码及方案测试结果))