springboot集成zookeeper

189 阅读8分钟

springboot集成zookeeper

导入依赖

<!--Curator-->
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>5.2.1</version>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>5.2.1</version>
</dependency>
<!--Zookeeper-->
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.8.0</version>
</dependency>

编写代码以及配置文件

@Data
@Component
@ConfigurationProperties(prefix = "curator")
public class WrapperZK {
    private int retryCount;
    private int elapsedTimeMs;
    private String connectString;
    private int sessionTimeoutMs;
    private int connectionTimeoutMs;
}
@Configuration
public class CuratorConfig {
    @Autowired
    private  WrapperZK wrapperZK;

    @Bean(initMethod = "start")
    public CuratorFramework curatorFramework() {
        return CuratorFrameworkFactory.newClient(
                wrapperZK.getConnectString(),
                wrapperZK.getSessionTimeoutMs(),
                wrapperZK.getConnectionTimeoutMs(),
                new RetryNTimes(wrapperZK.getRetryCount(), wrapperZK.getElapsedTimeMs()));
    }
}
curator:
  #重试retryCount次,当会话超时出现后,curator会每间隔elapsedTimeMs毫秒时间重试一次,共重试retryCount次。
  retryCount: 5
  elapsedTimeMs: 5000
  #服务器信息
  connectString: 127.0.0.1:2181
  #会话超时时间设置
  sessionTimeoutMs: 60000
  #连接超时时间
  connectionTimeoutMs: 5000

zookeeper实现分布式锁

分布式锁

分布式锁是控制分布式系统之间同步访问共享资源的一种方式

zookeeper分布式锁原理

zookeeper是基于临时顺序节点以及watcher监听器机制实现分布式锁

zookeeper的每一个节点都是一个天然的顺序发号器

在每一个节点下面创建临时顺序节点(EPHEMERAL_SEQUENTIAL)类型,新的子节点后面会加上一个次序编号,而这个生成的次序编号是上一个生成的次序编号加一.

例如,有一个用于发号的节点"test/lock"为父节点,可以在这个父节点下面创建相同的前缀的临时顺序节点,假定相同的前缀为"/test/lock/seq-".第一个创建的子节点基本上应该为/test/lock/seq-000000001,下一个节点为/test/lock/seq-000000002,依次类推.

微信截图_20230220160150.png

zookeeper节点的递增有序性可以确保锁的公平

一个zookeeper分布式锁,首先需要创建一个父节点,尽量是持久节点(PERSISTENT类型),然后每个要获得锁的线程都在这个节点下创建一个临时顺序节点,该节点是按照创建的次序一次递增的.

为了确保公平,可以简单的规定:编号最小的那个节点表示获得了锁.所以,每个线程在尝试占用锁之前,首先判断自己的序号是不是当前最小的,如果是则获取锁.

zookeeper的节点监听机制,可以保障占有锁的传递有序而且高效

每个线程抢占锁之前,先尝试创建自己的ZNode.同样,释放锁的时候需要删除创建的ZNode.创建成功后,如果不是序号最小的节点,就处于等待通知的状态.每一个等通知的ZNode,需要监视(watch)序号在自己前面的ZNode,以获取其删除事件.只要上一个节点被删除了,就进行再一次判断,看看自己是不是序号最小的那个节点,如果是,自己就获得锁,就这样不断地通知后一个ZNode节点.

另外,zookeeper的内部优越的机制,能保证由于网络异常或者其他原因,集群中占用锁的客户端失联时锁能够有效的被释放.什么机制呢,就是临时顺序节点,一旦占用ZNode锁的客户端与zookeeper集群服务器失去联系,这个临时的ZNode也将会自动删除.排在它后面的那个节点,也能收到删除事件,从而获得锁

也正是这个原因,zookeeper中不需要向redis那样考虑锁可能出现的无法释放的问题了,因为当客户端挂了,节点也挂了,锁也释放了.

zookeeper的节点监听机制,能避免羊群效应

zookeeper这种首尾相连,后面监听前面的方式,可以避免羊群效应.所谓的羊群效应就是一个节点挂掉,所有节点都去监听,然后做出反应,这样会给服务器带来巨大的压力.有了临时顺序节点以及节点监听机制,当一个节点挂掉,只有它后面的那一个节点才做出反应.

具体流程

  • 一把分布式锁通常使用一个ZNode节点表示;如果锁对应的ZNode节点不存在,首先创建ZNode节点.这里假设/test/lock,代表了一把需要创建的分布式锁.
  • 抢占锁的所有客户端,使用锁的ZNode节点的子节点列表来表示;如果某个客户端需要占用锁,则在/test/lock下创建一个临时顺序的子节点.比如,如果子节点的前缀是/test/lock/seq-,则第一次抢锁对应的子节点为/test/lock/seq-000000001,第二次抢锁对应的子节点为/test/lock/seq-000000002,以此类推.
  • 当客户端创建子节点后,需要进行判断:自己创建的子节点,是否为当前子节点列表中序号最小的子节点.如果是,则枷锁成功;如果不是,则监听前一个ZNode子节点变更消息,等待前一个节点释放锁.
  • 一旦队列中的后面的节点,获得前一个子节点变更通知,则开始进行判断,判断自己是否为当前子节点列表中序号最小的子节点,如果是,则认为加锁成功:如果不是,则持续监听,一直到获得锁.
  • 获取锁后,开始处理业务流程.完成业务流程后,删除自己的对应的子节点,完成释放锁的工作,以方便后继节点能捕获到节点变更通知,获得分布式锁.

独占锁&共享锁

上面讲的都是基于独占锁,那么能否实现共享锁呢?答案是可以的.

当操作是读请求,也就是要获取共享锁,如果没有比自己更小的节点,或比自己小的节点都是读请求,则可以获取到读锁.若比自己小的节点中有写请求,则当前客户端无法获取到读锁,只能等待前面的写请求完成.

如果操作是写请求,也就是要获取独占锁,如果没有比自己更小的节点,则表示当前客户端可以直接获取到写锁,对数据进行修改.如果发现有比自己更小的节点,无论是读操作还是写操作,当前客户端都无法获取到写锁,等待前面所有的操作完成.

Curator实现分布式锁

实际开发过程中,建议使用Curator客户端封装的API帮助我们实现分布式锁.

Curator是Netflix公司开源的一套zookeeper java客户端框架,相比于zookeeper自带的客户端zookeeper来说,Curator的封装更加完善,各种API都可以比较方便地使用.

这里使用Curator作为zookeeper的客户端实现.需要先导入依赖:

<!--Curator-->
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>5.2.1</version>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>5.2.1</version>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-client</artifactId>
    <version>5.2.1</version>
</dependency>

Curator的几种锁方案:

  • InterProcessMutex:分布式可重入排它锁
  • InterProcessSemaphoreMutext:分布式排他锁
  • InterProcessReadWriteLock:分布式读写锁
public class Demo {
    public static void main(String[] args) {
        CuratorFramework zkClient = ClientFactory.getClient();
        InterProcessMutex zkMutex = new InterProcessMutex(zkClient, "/test/mutex");

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程1启动");
                try {
                    zkMutex.acquire(); //阻塞等待,也可超时等待
                    System.out.println("线程1获取到锁");
                    Thread.sleep(2000);
                    zkMutex.release();
                    System.out.println("线程1释放锁");
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("线程2启动");
                try {
                    zkMutex.acquire();
                    System.out.println("线程2获取到锁");
                    Thread.sleep(2000);
                    zkMutex.release();
                    System.out.println("线程2释放锁");
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();

    }
}

zookeeper分布式锁的优缺点

这里把zookeepr与redis实现分布式锁对比下:

  • 优点:zookeeper分布式锁,除了独占锁,可重入锁,还能实现读写锁,并且可靠性比redis更好.
  • 缺点:zookeeper实现的分布式锁,性能不太高.因为每次在创建锁的过程和释放锁的过程中,都要动态创建,销毁瞬时节点来实现分布式锁功能.而zk中创建和删除节点只能通过leader服务器来执行,然后leader服务器还要将数据同步所有的follower机器上,同步之后才返回,这样频繁的网络通信,性能的短板是非常突出的;而redis则是异步复制

redis是ap架构,zookeeper是cp架构.在高性能,高并发的场景下,不建议使用zookeeper的分布式锁,可以使用redis分布式锁.而由于zookeeper的可靠性,所以在并发量不是很高的场景.推荐使用zookeeper分布式锁.

使用zk临时节点会存在另一个问题:由于zk依靠session定期的心跳来维持客户端,如果客户端进入长时间的GC,可能会导致zk认为客户端宕机而释放锁,让其他客户端获取锁,但是客户端在GC恢复后,会认为自己还持有锁,从而可能出现多个客户端同时获取到锁的情形.针对这种情况,可以通过jvm调优,尽量避免长时间GC的情况发生