zookeeper典型使用场景

  1. Zookeeper 非公平锁/公平锁/共享锁
  2. Leader 选举在分布式场景中的应用
  3. Spring Cloud Zookeeper注册中心实战

Zookeeper实现分布式锁

简述:
Zookeeper实现的分布式锁可以分为公平锁、非公平锁和读写锁;
Zookeeper主要利用节点无法重复创建和节点的监听通知机制来实现分布式锁;
节点无法创建特性是指:当一个线程创建了/lock节点后,其他线程再创建这个/lock节点会提示失败,因为zookeeper内部执行命令也像redis一样单线程执行,多个线程同时发送请求会排队执行,当请求创建节点时,如果发现节点已存在,则提示节点已存在;
节点的监听通知机制是指:如果获取锁的线程执行完逻辑删除节点释放锁,其它等待获取锁的线程监听此节点,如果该节点被修改(删除),那么其它节点将会收到通知,表示自己可以抢锁了,如果没有抢到锁,则继续监听/等待该节点;

Zookeeper和redis分布式锁的对比:
Redis是线程向master节点setnx存储数据成功就算获取锁成功,但当master节点还未来得及同步这个setnx数据时就宕机了,剩余的slave的节点会重新选举出一个新master,但是这个新master里没有setnx数据,这样造成这个setnx的所代表的分布式锁失效;(这个也可以解决,redis配置文件中有个配置是指定对少个slave节点同步数据成功,才返回客户端成功,但是这也不能达到百分之百的解决,前面我有详解,因为其选举算法);但是redis的效率比zookeeper高;
Zookeeper采用leader-follower模式,在创建锁节点时,只有半数以上的节点执行成功才算成功,就算leader节点挂了,其它follower节点中已经存在了锁节点,保证了数据的强一致性;但是因为这一点,相比于redis分布式锁性能较低,但是安全性高。

1、非公平锁:
使用了临时节点作为来实现的,为什么使用临时节点,是因为session过期后临时节点就会被zookeeper自动删除,如果在线程释放锁之前,因为某些原因比如突然停电宕机造成的程序中断,那么这个临时节点可以自动进行删除,不然这个锁不进行人工手动删除将会永远存在。
非公平锁的加锁原理:
在这里插入图片描述
非公平锁在高并发的场景下,性能下降的比较厉害,因为所有的线程对同一个/lock节点进行监听,当被监听的释放锁删除/lock节点后,zookeeper会通知所有监听/lock节点的客户端线程,所有的监听线程收到事件,再次并发竞争创建/lock节点,一次只能有一个线程竞争成功,其它线程竞争失败,再次等待,这就是羊群效应。这种羊群效应就是需要zookeeper需要大量通知其它线程,多次并发创建锁,而其不止一次,会造成资源的浪费和性能的下降;为了避免羊群效应,可以采用公平锁。
2、公平锁:
公平锁的实现主要是使用了临时顺序节点,和上面的原因一样,当seesion过期时会被zookeeper自动删除,减少了人工维护的成本,但是其父节点是使用的container节点,这种节点的是其下面如果子节点都被删除,则container节点会被zookeeper删除;
公平锁的实现原理图:
在这里插入图片描述

(1)一个线程要获取锁,如果发现没有/节点lock,则会立即创建一个container类型的/lock节点,再在/lock节点下创建一个临时顺序节点;如果发现已经有了/lock节点,则在lock节点下创建一个临时顺序节点;
(2)判断自己是不是lock节点下最小的顺序节点,如果是最小的,则获取锁,如果不是,则对前面的上一个节点进行监听;
如何判断是不是最小的节点:(在java代码中进行)取出当前/lock节点下所有节点进行排序,放在list中,判断当前线程创建的临时节点是不是最小的;
注:因为是在java内存中获取当前比自己小的上一个节点,如果还没来得及监听此最小节点就删除,进行监听时也会收到zookeeper反馈,再重复执行(2)步;
(3)获得锁的请求,处理完释放锁之后,即delete自己创建的顺序节点,然后后继的第一个节点收到监听通知,重复(2)布的判断。
这样公平锁的实现方式就避免了 非公平锁的羊群效应,因为释放锁后只会通知一个线程。

关于公平锁的的几个问题:
问题1:当前线程怎么判断自己能不能获得锁?

在公平锁的实现中,只有顺序最小的节点才会获得锁,每当一个线程收到监听消息后,就会唤醒等到的线程,然后获取这个/lock节点下所有的子节点进行排序,比较当前节点是不是列表中序号最小的节点,如果是,则获取锁,如果不是,则监听上一个比自己小的节点。
问题2:排队的某个子节点如果因为session超时被删除,整个监听序列会不会出现问题?
不会,因为如果中间某个节点删除,则会通知后面一个监听此节点的节点,会执行上面执行流程的第(2)步,会重新监听比顺序比自己小的上一个节点。
问题3:顺序节点已经创建,但是响应客户端失败,认为自己创建失败,造成多次创建怎么办?
客户端发送创建节点命令,服务端收到命令并成功创建节点,但是给客户端响应成功的时候,客户端和服务器发生网络中断,但是又在session超时之内又重新连接上了,此时客户端没有收到创建成功的响应,认为创建节点失败,由于客户端重试机制的存在,会重新发起重建,但是上一个多创建的节点又不会被删除,这样就导致一个线程多次节点的创建,多创建的节点一直存在服务器中,不会被释放,这就是创建了所谓的幽灵节点或者僵尸节点,那么如何避免创建所谓的幽灵节点呢; 可以通过Curator客户端的protection模式来解决幽灵节点的问题。Protection模式会为创建的顺序节点加一个uuid,(uuid会缓存在内存中),在进行重试的时候会判断在/lock下的子节点有没有包含这个uuid的子节点,如果已存在,表示已创建,不会再创建节点。

framework.create()
        //防止幽灵节点,curator加上这个就可以了
        .withProtection()
        .withMode(CreateMode.PERSISTENT).forPath(nodePath, "mydata".getBytes())

3、读写锁(共享锁):
读写锁就是,读写和写写互斥,读读不互斥;
Zookeeper对读写锁的原理如下:
在这里插入图片描述

① read请求:如果前面都是read请求,则直接获取锁(读读共享),如果前面有write请求,则该read不能获取锁(读写互斥),需要对前面最近的write请求进行监听。
② write请求:如果当前节点是最小节点,则可以获取锁,如果有节点,无论read还是write都不能获取锁(读写互斥、写写互斥),需要监听前面最近的一个节点。
描述:curator对读写锁做了实现
就是给读写两种请求创建的节点都带有读写标识;
读请求:拿到容器中的所有结点,如果前面存在write请求,则监听前面最近一个write节点,当这个节点释放后尝试获取锁;如果前面都是read请求,则直接获取锁;
写请求:和上面Curtor描述的公平锁逻辑一致,不在区分读写节点。

Curator客户端实现zookeeper分布式锁:因为时间原因,其源码我也没看,但是其实现原理就是上面所述。


@Configuration
public class CuratorConfig {
    @Bean(initMethod = "start")
    public CuratorFramework curatorFramework() {
        ExponentialBackoffRetry retry = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework curatorFramework = CuratorFrameworkFactory.newClient("192.168.244.134:2181", retry);
        return curatorFramework;
    }
}


@Slf4j
@RestController
public class CuratorLockDemoController {
    @Autowired
    private CuratorFramework curatorFramework;

    @GetMapping("/stock/reduce")
    public Object reduceStock(Integer id) throws Exception {
        InterProcessLock interProcessLock = new InterProcessMutex(curatorFramework, "/lockKey1" + id);
        try {
            //添加分布式锁
            interProcessLock.acquire();
            //减库存
            Thread.sleep(100);
        } finally {
            //释放分布式锁
            interProcessLock.release();
        }
        return "成功";
    }
}

Zookeeper-使用Curator客户端实现Leader选举的功能

一、在分布式环境中,相同的业务应用分布在不同的机器上,有些业务逻辑(例如一些耗时的计算,网络I/O处理),往往只需要让整个集群中的某一台机器进行执行,其余机器可以共享这个结果,这样可以大大减少重复劳动,提高性能,于是这Leader选举便是这种场景下的碰到的主要问题。
二、当Leader服务器发生故障的时候,系统能够快速地选出下一个ZooKeeper服务器作为Leader。一个简单的方案是,让所有的Follower监视leader所对应的节点。当Leader发生故障时,Leader所对应的临时节点会被自动删除,此操作将会触发所有监视Leader的服务器的watch。这样这些服务器就会收到Leader故障的消息,进而进行下一次的Leader选举操作。但是,这种操作将会导致“从众效应”的发生,尤其是当集群中服务器众多并且宽带延迟比较大的时候更为明显。在ZooKeeper中,为了避免从众效应的发生,它是这样来实现的:每一个Follower为Follower集群中对应着比自己节点序号小的节点中x序号最大的节点设置一个watch。只有当Followers所设置的watch被触发时,它才惊醒Leader选举操作,一般情况下它将成为集群中的下一个Leader。很明显,此Leader选举操作的速度是很快的。因为每一次Leader选举几乎只涉及单个Follower的操作。
curator对原生api进行了封装,将节点创建,事件监听和自动选举过程进行了封装,我们只需要调用API即可实现Master选举。分别使用LeaderSelector和LeaderLatch两种方式,进行server模拟真实的运行场景。(LeaderLatch这里不做演示了)

LeaderSelector
LeaderSelector是利用Curator中InterProcessMutex分布式锁进行抢主,抢到锁的即为Leader。
1.org.apache.curator.framework.recipes.leader.LeaderSelector
//开始抢主
void start()
//调用确保此实例在释放领导权后还可能获得领导权
void autoRequeue()

2.LeaderSelectorListener是LeaderSelector客户端节点成为Leader后回调的一个监听器,在takeLeadership()回调方法中编写获得Leader权利后的业务处理逻辑。
org.apache.curator.framework.recipes.leader.LeaderSelectorListener
//实例被选为leader之后,调用takeLeadership方法进行业务逻辑处理,处理完成即释放领导权
void takeLeadership()
3.LeaderSelectorListenerAdapter是实现了LeaderSelectorListener接口的一个抽象类,封装了客户端与zk服务器连接挂起或者断开时的处理逻辑(抛出抢主失败CancelLeadershipException),一般监听器推荐实现该类。
代码演示:


@Slf4j
public class CuratorLeadSelectorDemo {
    public static void main(String[] args) throws Exception {
        ExponentialBackoffRetry retry = new ExponentialBackoffRetry(1000, 3);
        ArrayList<Thread> list = new ArrayList<>();
        //模拟五个server进行选举一次性执行选出一个learder,但执行完takeLeadership方法后,释放领导权,
        //autoRequeue()方法可以重新参加leader选举
        for (int i = 0; i < 5; i++) {
            CuratorFramework curatorFramework = CuratorFrameworkFactory.newClient("192.168.244.134:2181", retry);
            curatorFramework.start();
            Thread thread = new Thread(() -> {
                LeaderSelector leaderSelector = new LeaderSelector(curatorFramework, "/selector_path", new LeaderSelectorListenerAdapter() {
                    @Override
                    public void takeLeadership(CuratorFramework client) throws Exception {
                        Thread.sleep(3000);
                        log.info("当前我是leader,leaderSelector={},执行完毕释放领导权", this);
                    }
                });
                log.info("启动选举...");
                //调用确保此实例在释放领导权后还可能获得领导权
                leaderSelector.autoRequeue();
                leaderSelector.start();
            });
            list.add(thread);
        }
        list.parallelStream().forEach(t -> {
            t.start();
        });
        Thread.sleep(Integer.MAX_VALUE);
    }
}

zookeeper注册中心

什么是注册中心?
顾名思义,就是让众多的服务,都在Zookeeper中进行注册,啥是注册,注册就是把自己的一些服务信息,比如IP,端口,还有一些更加具体的服务信息,都写到 Zookeeper节点上, 这样有需要的服务就可以直接从zookeeper上面去拿,怎么拿呢? 这时我们可以定义统一的名称,比如,User-Service, 那所有的用户服务在启动的时候,都在User-Service 这个节点下面创建一个子节点(临时节点),这个子节点保持唯一就好,代表了每个服务实例的唯一标识,有依赖用户服务的比如Order-Service 就可以通过User-Service 这个父节点,就能获取所有的User-Service 子节点,并且获取所有的子节点信息(IP,端口等信息),拿到子节点的数据后Order-Service可以对其进行缓存,然后实现一个客户端的负载均衡,同时还可以对这个User-Service 目录进行监听, 这样有新的节点加入,或者退出,Order-Service都能收到通知,这样Order-Service重新获取所有子节点,且进行数据更新。这个用户服务的子节点的类型为临时节点。
Zookeeper中临时节点生命周期是和SESSION绑定的,如果SESSION超时了,对应的节点会被删除,被删除时,Zookeeper 会通知对该节点父节点进行监听的客户端, 这样对应的客户端又可以刷新本地缓存了。当有新服务加入时,同样也会通知对应的客户端,刷新本地缓存,要达到这个目标需要客户端重复的注册对父节点的监听。这样就实现了服务的自动注册和自动退出。
在这里插入图片描述
Spring Cloud 生态也提供了Zookeeper注册中心的实现,这个项目叫 Spring Cloud Zookeeper 下面我们来进行实战。

引入依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>

添加配置

server:
  port: 8080
spring:
  application:
    name: my-application

  cloud:
    zookeeper:
      connect-string: 192.168.244.134:2181

启动项目可以看到在zookeeper注册的服务:
在这里插入图片描述
可以看到zookeeper把服务都注册在/services的节点下,子节点为/services/my-application,是以服务名为节点的,这个服务的每个实例都是在以生成的UUID作为最后的子节点,这个子节点里存储了当前实例的相关数据。这样服务启动的时候可以拉去zookeeper中所有的服务信息缓存在本地,以备调用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值