【分布式】基于Zookeeper实现分布式锁、秒杀问题复盘

分布式


分布式锁解决方案 — 乐观锁和悲观锁的选择、tomcat并发、缓存一致、基于Zookeeper实现分布式锁、高并发抢红包系统复盘


还要加快一点,时间来不及了… redis、 mq、锁🔒redisson等技术重构cfeng.net 成为一个可以分布式部署的项目

乐观锁和悲观锁 选择

前面的分布式锁分别使用了数据库级别的乐观锁、悲观锁和redis实现,谈了实现,这里cfeng补充一点乐观锁、悲观锁的选择问题,解释他们各自的问题(当然基本的乐观锁适合读多写少,悲观锁适合写多读少基本的都know】

首先最基本的需要明确: 乐观锁是无锁结构,悲观锁是一种思想,加上显式的锁解决冲突问题,比如锁代码块的java提供的synchronized,锁数据的数据库提供的行锁、表锁、X锁、S锁等

乐观锁 —海量写请求大量失败

乐观锁的两种基本实现 CAS (ABA还是要version才能解决)和 version版本号机制,本身是没有加锁的,就像之前Cfeng的提现请求(重复提交问题),当然可以很好解决,也就是只有最先到达的就更新成功,同时操作的系统的线程就可能失败

而如果是并发秒杀, 1000个不同的用户秒杀100件商品,如果不是使用提前到redis缓存中处理减库存,直接在数据库中操作,使用Jmeter测试,如果加上乐观锁,虽然可以防止超卖,但是售出的远远不到100个 【explain: 如果同时有10个线程查询到版本号为1,其中一个线程更新成功,变为2,那么其余9个都失败了,所以1/10的请求有效】 大量的请求都返回抢购失败,影响用户体验,也影响商家(没卖完)

所以乐观锁适合的是并发量较小的、修改少的传统系统,可以防止几个人同时更改数据, 乐观锁因为无锁,不需要额外的锁操作开销,同时也可以提高并发量,吞吐量

需要注意,乐观锁为无锁,使用乐观锁需要注意不要乱用数据库事务@Transactional, 比如mysql默认的READ_REPEATEABALE,本身就调用了相关的数据库锁; 所以要么就使用最低的事务隔离级别read_uncommitted; 或者不使用事务,去掉@Transactional

同时解释事务和 锁的关系: 事务是一种粗粒度的对于数据库表的并发限制,保证数据操作正确,但是不能保证业务操作正确,事务ACID

悲观锁 ---- 海量请求 单线程依次执行 响应慢

悲观锁无论是代码级别的锁,还是数据库排他锁for update,都是只能单线程操作,当前线程操作的时候,其他线程都是不能操作的,处于阻塞状态,这样可能让后面的请求进行长时间的阻塞等待,在用户视角就是一直等待页面响应(explain:如果同时1000个线程同时操作一个悲观锁加持的片段,获取到释放需要20ms,那可能需要等待1000 * 20 = 20s,长时间的阻塞等待、假死】,所以需要使用redis缓存加快访问速度

悲观锁主要就是主键情况下加行锁排他锁锁数据,保证同时只有一个线程更新,为了提高用户体验,可以在请求达到后台时,进行限流,比如让请求全部进入rabbitMQ,逐步被操作; 或者可以使用令牌桶进行限流

令牌桶限流

rabbitMQ限流的方式就是Producer将所有的请求发送给消费者逐步处理, 而令牌桶算法就是防止阻塞,进行流量限制,让流量均匀发送。

大小固定的令牌桶以恒定的速率不断产生令牌,如果令牌没有消耗,或者小于产生的速率,那么令牌就会将桶填满,再产生的令牌就会从桶中溢出,保持最大量有限

在这里插入图片描述

而在项目中,可以直接借助Guava提供的RateLimiter实现令牌桶限流接口

Guava是Google开源工具类,提供了限流工具类RateLimiter进行令牌桶算法方式的限流,可以直接引入依赖,使用

<dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>${guava.version}</version>
        </dependency>

之后在后台的请求接口中使用RateLimiter进行限流

@RequestMapping("/account")
public class UserAccountController {

    private final UseAccountService accountService;

    //令牌桶方式限流,这里借助工具类,每s放行10个请求
    RateLimiter rateLimiter = RateLimiter.create(10);

    //方便测试,这里就直接使用参数的方式接收
    @GetMapping("/getMoney")
    public ResponseEntity<String> getMoney(UserAccountDto userAccountDto) {
        //阻塞式获取令牌
//        log.info("等待时间" + rateLimiter.acquire());
        //非阻塞式获取令牌
        if(!rateLimiter.tryAcquire(1000, TimeUnit.MILLISECONDS)) {
            log.warn("被限流了,直接返回失败,说明没库存了");
            return new ResponseEntity<>("获取数据失败,被限流了",HttpStatus.OK);
        }
        if(Objects.isNull(userAccountDto.getAccount()) || Objects.isNull(userAccountDto.getUserId())) {
            return new ResponseEntity<>("参数错误",HttpStatus.BAD_REQUEST);
        }
        try {
//            accountService.takeMoney(userAccountDto);
//            accountService.takeMoneyWithVersion(userAccountDto);
//            accountService.takeMoneyWithLock(userAccountDto);
//            accountService.takeMoneyWithRedisLock(userAccountDto);
            accountService.takeMoneyWithZookeeper(userAccountDto);
            return new ResponseEntity<>("提现成功", HttpStatus.OK);
        } catch (Exception e) {
            return new ResponseEntity<>("出现错误",HttpStatus.NOT_FOUND);
        }
    }
}

RateLimiter rateLimiter = RateLimiter.create(10); 初始化令牌桶类,每s放行10个请求, 而请求获取 令牌也有两种方式:

  • 阻塞式获取令牌: 请求线程进入后,如果令牌桶内没有足够多的令牌,就在此处阻塞,等待令牌的发放
  • 非阻塞式获取令牌: 请求线程进入后,如果令牌桶没有足够的令牌,会尝试等待设置好的时间(这里1000ms),如果还是没有拿到令牌,那么直接返回请求失败(因为前面已经发行了足够多的请求进入服务参与秒杀, 这些请求就算进入都是失败的),如果timeout设置0,那么就式阻塞式

【相关的原理源码后续看情况解释】

测试发现1000个线程/1s,在配合Zookeeper分布式锁 + RateLimiter接口限流 (虽然因为机器原因还是5%的请求挂了),大部分请求直接在controller限流位置非阻塞位置返回,没有进入service执行,最终只有一个请求执行成功

Zookeeper注册中心 intro

分布式架构项目 — 系统拆分、独立部署、服务模块解耦;随之而来的分布式事务、数据一致性、分布式架构下的并发安全问题; 除了可以利用数据库级别的乐观、悲观锁;以及借助redis原子性SETNX实现分布式锁,还可以利用注册中心Zookeeper实现分布式锁

Zookeeper的功能特性:

  • 顺序一致性: 从同一个客户端发起的请求,最终严格按照顺序应用到ZooKeeper
  • 原子性: 所有的事务请求结果在整个集群所有机器上的应用情况一致,整个集群中所有机器的状态一致
  • 单一系统映像:ZooKeeper集群中每一台ZooKeeper机器中数据模型相同(冗余)
  • 可靠性 : 一旦更改被应用,更改结果就会持久化,直到下一次覆盖

Zookeeper是 分布式的服务协调中间件(注意其他的分布式服务minIO、rabbitMQ),采用同一的协作方式管理各个子系统(之前Cfeng的博客简单提过RPC框架Dubbo服务发现), 最终使分布式系统像一个动物园一样

在这里插入图片描述

节点: Zookeeper中节点分为 构成集群的机器的 机器节点 以及 数据模型中的数据单元的数据节点; 不管是机器节点还是数据节点都是ZNode

ZooKeeper将所有的数据存储在内存中,最终的数据模型就构成了一颗树,和linux的文件系统一样,构成一棵文件树,节点 对应的就是路径 ; 每个节点保存着相关的信息

临时节点: 生命周期和客户端会话绑定在一起,客户端会话失效,节点就会删除,创建ZNode可以带上标识(整型数字)

持久节点: ZNode被创建了,除非主动移除ZNode,否则就一直都在Zookeeper,和客户端会话没有关系

Watcher监听器: 事件监听器,ZooKeeper允许用户在指定的ZNode创建监听事件,一旦事件触发,Zookeeper就会通知到相关的Client

Zookeeper是高可用的,Zookeeper集群,一般部署奇数个实例,当其中leader挂掉,就会重新选举产生新的Leader【所以Zookeeper和Redis有很多相似之处,包括分布式锁】

Zookeeper作为 典型的分布式数据一致性的解决方案,可以基于ZooKeeper实现数据发布/订阅、负载均衡、命名服务、分布式协调通知、集群管理、分布式锁、分布式队列

统一配置管理: 每一个子系统都需要的配置文件统一放置到Zookeeper的ZNode节点

统一命名服务: 通过ZNode进行统一命名,各个子系统可以通过名字获取到节点上相应的资源

分布式锁: 通过创建与该 共享资源 相关的 顺序临时节点 与 动态监听机制, 控制多线程对共享资源的并发访问 ( 临时节点是核心)

集群状态: 可以动态感知节点的CRUD,保证集群下 的相关节点的主、副本的一致性

Zookeeper的典型应用场景就是配合Dubbo作为注册中心; 服务生产者将子集提供的服务注册到ZooKeeper中心,服务消费者进行服务调用时先在Zookeeper中查找服务,获取到服务生产者的详细信息后,在去调用服务生产者提供的接口

 |==========--------------> 注册中心Zookeeper <-- 发布暴露服务,接口方法签名--------------------|
消费者  <-----------------异步通知-|         直接通过HTTP调用,或者RPC远程协议(DUBBO)        生产者(注册发布服务系统)
【订阅调用服务系统】																		 |
   |																					|
   =============================> 监控器 <======================================================

虽然目前使用SpringCloud较多,但是Dubbo也还是有可用之处,后期Cfeng可能会分享基于Dubbo重构微服务项目(不使用SpringCloud)

SpringBoot整合ZooKeeper

整合Zookeeper首先需要安装Zookeeper,直接到官网下载即可,如果是Windows版本,那么就直接安装到本地,点击bin下埋你的zkServer.cmd就可以运行ZooKeeper服务端 【和redis-Windows类似,mongoDB需要以命令行方式指定dbpath启动】

首先需要引入相关的依赖

<!-- zookeeper依赖  还有curator框架-->
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.6.2</version>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-log4j12</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>4.2.0</version>
        </dependency>

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

springboot要使用zookeeper服务,也是通过网络进行连接该服务,引入依赖之后,就在yml中配置相关的依赖项

#### zookeeper的配置 -- 主机和命名空间, 命名空间用于区分不同的机器
zk:
  host: 127.0.0.1:2181
  namespace: cfengHost

这里是自动配置的,高度封装的Curator框架,使用该框架实例可以解决底层的问题: 重连、反复注册、NodeExistsException

在SpringBoot项目中,可以自定义注入CuratorFramework实例 对Zookeeper进行相应的操作,创建一个Zookeeper的配置类管理配置

//这里也是使用Environment对象来获取配置文件中的内容
 * 自定义配置CuratorFramework对象用来实现Zookeeper的快速操作使用
 */

@Configuration
@RequiredArgsConstructor
public class ZooKeeperConfig {

    private final Environment environment;

    /**
     * 自定义注入Bean-Zookeeper高度封装的客户端Curator实例
     * 采用工厂模式进行创建,指定客户端连接到Zookeeper服务端的策略: 采用重试机制 5次,每次间隔1s 100ms
     */
    @Bean
    public CuratorFramework curatorFramework() {
        CuratorFramework curatorFramework = CuratorFrameworkFactory.builder().connectString(Objects.requireNonNull(environment.getProperty("zk.host"))).namespace(Objects.requireNonNull(environment.getProperty("zk.namespace")))
                .retryPolicy(new RetryNTimes(5,1000)).build();
        curatorFramework.start();
        return curatorFramework;
    }
}

配置Zookeeper操作对象CuratorWork就可以借助该对象进行节点的各种操作完成Zookeeper的功能

启动项目,可以看到ZooKeeper的相关的日志信息

rg.apache.zookeeper.ZooKeeper           : Client environment:zookeeper.version=3.6.2--803c7f1a12f85978cb049af5e4ef23bd8b688715, built on 09/04/2020 12:44 GMT
2022-09-21 15:29:55.690  INFO 9176 --- [           main] org.apache.zookeeper.ZooKeeper           : Client environment:host.name=DESKTOP-4A4BD0R
2022-09-21 15:29:55.690  INFO 9176 --- [           main] org.apache.zookeeper.ZooKeeper           : Client environment:java.version=16

Zookeeper分布式锁

Zookeeper作为注册中心进行服务发布、订阅,还可以利用创建 与 共享资源 相关的 顺序临时节点, 采用Zookeeper提供的监听器Watcher机制,控制多线程的共享资源的并发访问

ZooKeeper架构通过冗余实现高可用性,当其中一个Zookeeper无应答,就询问另外一个主机,Zookeeper将数据存储在一个分层的命名空间(类似linux的文件系统),类似前缀树结构,客户端可以在节点进行读取

其实可以按照redis的实现思路来理解,核心过程:

  1. 操作共享资源前加上锁 ----- 创建临时顺序节点,利用Watcher监听器不断监听到最小序号的节点
  2. 获取锁成功就可以进行操作
  3. 操作完成 当前线程释放锁 — 删除对应的最小序号的临时节点,让其他的节点序号变为最小,其对应的线程获取到锁 【可重入、高可用、不死锁、互斥、公平】

在zookeeper指定节点locks下面创建临时节点node_n

获取locks下面的所有的子节点children

对子节点按照自增序号从小到大排序

判断本节点是否为第一个节点(序号最小),是则获取锁,不是就监听Watcher比其小节点的删除时间

若删除事件发生,回到前面重新判断,直到获取到锁

【所以和redis一样存在一个等待队列,所以是可重入的】

在这里插入图片描述

核心就是创建临时顺序节点和采用Watcher机制监听临时节点的增减和删除

而在具体实现中,还可以直接使用ZooKeeper的InterProcessMutex(进程间 互斥)实现分布式锁【 InterProcessMutext为公平的可重入锁】

在这里插入图片描述

InterProcessMutex 不依赖JVM,所以可以用作分布式锁

//基于zookeeper实现分布式锁
    @Override
    public void takeMoneyWithZookeeper(UserAccountDto userAccountDto) throws Exception {
        //主要依赖的是ZooKeeper的InterProcessMutex类进程互斥
        //Znode的节点路径
        final String pathPrefix = "/middleware/zkLock";
        //创建Zookeeper的进程互斥锁实例,尝试获取锁的最长的事件为20s,这里创建和资源相关的锁
        InterProcessMutex mutex = new InterProcessMutex(zookeeperClient,pathPrefix + userAccountDto.getUserId() + "-lock");
        try {
            //当前线程对应的节点尝试获取锁,获取的最大等待时间20s
            if(mutex.acquire(10L,TimeUnit.SECONDS)) {
                //当前线程获取到锁
                UserAccount userAccount = userAccountMapper.selectByUserId(userAccountDto.getUserId());
                if(!Objects.isNull(userAccount) && userAccount.getAmount().doubleValue() > userAccountDto.getAccount()) {
                    //提现
                    int res = userAccountMapper.updateAmount(userAccountDto.getAccount(),userAccountDto.getUserId());
                    if(res > 0) {
                        //提现成功
                        UserAccountRecord record = new UserAccountRecord();
                        record.setCreateTime(LocalDateTime.now());
                        record.setAccountId(userAccount.getId());
                        record.setMoney(BigDecimal.valueOf(userAccountDto.getAccount()));

                        accountRecordMapper.insert(record);
                        log.info("zookeeper分布式锁 -当前待提现金额为:{},账户的余额为: {}, 成功", userAccountDto.getAccount(), userAccount.getAmount());
                        //成功后结束
                    } else {
                        throw new Exception("更新出现异常");
                    }
                } else {
                    throw new Exception("账户余额不足");
                }
            } else {
                throw new Exception("获取分布式锁失败Zookeeper");
            }
        } catch (Exception e) {
            throw e;
        } finally {
            //为了保证不死锁,不管正常还是异常,都需要释放锁
            mutex.release();
        }
    }

这里最关键首先就是创建一个Mutext节点,使用当前节点获取锁,获取到锁之后使用之后一定要正常释放,使用finally即可,和redis类似

ZooKeeper的分布式锁主要就是创建节点ZNode,所以这种方式具有时间开销,需要耗费一定时间进行节点的CRUD, 这种开销可以通过集群部署多个ZooKeeper缓解

高并发抢红包使用Zookeeper分布式锁

之前高并发抢红包为了避免同一个用户抢到多个红包,使用redis的SETNX作为分布式锁,这里可以再次使用ZooKeeper分布式锁来代替

为了能够高效的进行相应,红包生成后是直接放在缓存中,异步方式将相关记录计入数据库

/**
     * 抢红包核心业务,需要首先在缓存中查找红包并且进行记录,抢到红包后需要调用DetailService写入数据库
     * @param userId
     * @param redId
     * @return
     * @throws Exception
     */
    @Override
    public BigDecimal rob(Integer userId, String redId) throws Exception {
        //Redis操作组件
        ValueOperations valueOperations = redisTemplate.opsForValue();
        //抢红包前先判断是否抢过红包,如果已经抢过了,就直接返回红包金额,前台显示即可 redId : userID : rob  这里是抢到的ID
        Object object = valueOperations.get(redId + ":" + userId + ":rob");
        if(object != null) {
            return new BigDecimal(object.toString());
        }
        //点红包 --- 判断缓存系统中是否有红包剩余,> 0
        Boolean res = clik(redId);
        //有红包则进入拆红包业务逻辑
        if(res) {
            //分布式锁,每一个人一次只能抢到一次红包,一对一关系
//            final String lockKey = redId + ":" + userId + "-lock";
            final  String lockKey = "/middleware/zkLock/" + redId + "-" + userId + "-lock";
            InterProcessMutex mutex = new InterProcessMutex(zookeeperClient,lockKey);
            //从小红包列表中拆一个红包

            //调用setIfAbsent方法,实现分布式锁,也就是这里pop是只能一次弹一个
//            Boolean lock = valueOperations.setIfAbsent(lockKey,redId);
            //设置分布式锁过期时间24小时
//            redisTemplate.expire(lockKey,24L,TimeUnit.HOURS);

            try {
                //判断当前线程是否获取到了锁
//                if(lock) {
                //这里采用zookeeper获取锁
                if(mutex.acquire(10L,TimeUnit.SECONDS)) {
                    Object value = redisTemplate.opsForList().rightPop(redId);
                    //如果红包金额不为空,说明有红包,有钱
                    if(value != null) {
                        //抢到一个,总数减少
                        String redTotalKey = redId + ":total";
                        //获取当前总数,如果为空则总数为0, 同时更新; 但这里的操作不是但命令,不具有原子性,会出现安全问题,需要修改
//                        Integer currentTotal = valueOperations.get(redTotalKey) != null ? (Integer)valueOperations.get(redTotalKey) : 0;
//                        valueOperations.set(redTotalKey,currentTotal - 1);
                        valueOperations.increment(redTotalKey,-1);

                        //处理红包金额为元
                        BigDecimal result = new BigDecimal(value.toString()).divide(new BigDecimal(100));
                        //抢到红包的信息记录进入数据库
                        redDetailService.recordRobedPacket(userId,redId,new BigDecimal(value.toString()));
                        //当前用户抢过红包了,使用Key进行标识,设置过期时间为1天
                        valueOperations.set(redId + ":" + userId + ":rob",result,24L, TimeUnit.HOURS);
                        log.info("当前用户抢到红包了:剩余红包数量:{},userId={} key={} 金额={}",valueOperations.get(redTotalKey),userId,redId,result);

                        return result;
                    }
                }
            } catch (Exception e) {
                throw new Exception("加分布式锁失败");
            } finally {
                //分布式锁必须释放
//                if(Objects.equals(redId,valueOperations.get(lockKey))) {
//                    //释放当前线程的redis的锁key
//                    redisTemplate.delete(lockKey);
//                }
                mutex.release();
            }
        }
        //null表示当前用户没有抢到红包
        return null;
    }

    /**
     * 点红包业务逻辑: 判断缓存系统是否有红包
     */
    private Boolean clik(String redId) throws Exception {
        ValueOperations valueOperations = redisTemplate.opsForValue();
        //获取红包总数 key:total
        String redTotalKey = redId + ":total";
        //获取剩余个数
        Object total = valueOperations.get(redTotalKey);
        //判断剩余个数是否大于0
        if(total != null && Integer.valueOf(total.toString()) > 0) {
            //还有红包
            return true;
        }
        return false;
    }

当然实际的项目中,第一次查询的时候会先将库存加载到缓存中

Tomcat并发量有限制

这里发起13000线程/1s, 出现的结果是部分线程报错:

org.apache.http.conn.HttpHostConnectException: Connect to 127.0.0.1:8081 [/127.0.0.1] failed: Connection refused: connect
	at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:156)

在这里插入图片描述

只有一半的请求请求成功,虽然那一半的请求确实也是正确的(10个人抢到了红包),且符合使用缓存的情况,1453个请求每秒 ,比不使用缓存性能提升了好几倍

而至于为什么一半的请求全部挂掉了,就是因为SpringBoot内置的Tomcat 最大并发数有限(Tomcat本身也是一个应用,需要考虑它能够抗住高并发),不然请求挂在Tomcat,进入不了应用也白忙活了【RabbitMQ也有心无力,都没有进入】

可以修改yml配置文件,手动”天真的“ 设置最大并发数和最大连接数,手动调优【因为确实还是扛不住】 — 所以这里就引出 进一步的处理方式 ---- Nginx控制流量

server:
  tomcat:
    threads:
      max: 13000  #tomcat可承受最大并发数,默认200 【再大可能机器扛不住】
    max-connections: 13000  #最大连接数,默认8972

事实证明,这也确实很天真,再次发起请求,还是崩了,Error的达到了59%,吞吐量还是1400左右,还是只有少部分请求请求成功,一个Tomcat扛不住,搭建集群还是不可能抗住上百万的流量,所以Nginx必须限流 — 保证到达Tomcat容器的流量在可承受范围内

再给个解决方案: Cache aside

缓存与数据库双写一致性

缓存虽然确实可以提高访问速度、增大系统吞吐量,降低数据库压力,保证应用平稳运行,但是也可能带来问题, 其中一个问题: 数据库和缓存的一致性

延时双删: 先淘汰缓存、写数据库,休眠1s,再次淘汰Cache; 【可以删除1s内缓存的脏数据】

后面具体遇到强一致性业务场景再详解🎄

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
### 回答1: 使用 ZooKeeper 实现分布式锁最常见的方法是使用公平锁(Fair Lock)。在这种情况下,在 ZooKeeper 集群中创建一个特殊的临时节点,并使用它来保持锁定状态。当一个客户端尝试获取锁时,它会尝试创建这个临时节点,如果节点创建成功,则说明该客户端已经获得了锁。 使用 ZooKeeper 进行分布式锁实现需要使用 ZooKeeper 的原子操作,例如创建节点和监视节点,以保证锁的正确性。 代码示例如下: ```java public class DistributedLock { private ZooKeeper zk; private String lockNode; private String lockPath; public DistributedLock(ZooKeeper zk, String lockNode) { this.zk = zk; this.lockNode = lockNode; this.lockPath = "/locks/" + lockNode; } public void lock() throws Exception { while (true) { try { zk.create(lockPath, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); return; } catch (KeeperException.NodeExistsException e) { waitForLock(); } } } public void unlock() throws Exception { zk.delete(lockPath, -1); } private void waitForLock() throws Exception { CountDownLatch latch = new CountDownLatch(1); Watcher lockWatcher = new Watcher() { @Override public void process(WatchedEvent event) { if (event.getType() == EventType.NodeDeleted) { latch.countDown(); } } }; zk.exists(lockPath, lockWatcher); latch.await(); } } ``` 该代码示例提供了一个简单的实现,可以作为创建自己的分布式锁的基 ### 回答2: 在基于 ZooKeeper 实现 Java 分布式锁的过程中,可以按照以下步骤进行: 1. 创建一个基于 ZooKeeper 的客户端对象。 2. 在 ZooKeeper 上创建一个持久化的节点,作为锁的根节点。 3. 当需要进行锁操作时,创建一个临时有序节点作为当前请求的锁节点。 4. 调用 `getChildren` 方法获取锁根节点下的所有节点,并根据节点的序号进行排序。 5. 检查当前节点是否是锁根节点下序号最小的节点,如果是则获取到锁,执行业务逻辑。 6. 如果当前节点不是锁根节点下序号最小的节点,则注册监听锁根节点下序号比自己小一的节点。 7. 当监听到序号比自己小一的节点删除时,重复步骤 4-6 直到获取到锁。 8. 业务逻辑执行完成后,可以删除当前节点,释放锁资源。 此外,还需要特别注意以下几个问题: 1. 分布式锁的超时机制:在创建临时节点时,可以指定一个超时时间,当超过该时间后,如果还未获取到锁,可以删除当前节点,避免死锁。 2. 锁的释放:在业务逻辑执行完成后,需要手动删除当前节点。如果由于某些原因未能正常删除,则需要提供一种机制,在锁节点创建时设置一个 TTL(time-to-live),让 ZooKeeper 在锁节点过期后自动删除。 3. 锁节点的竞争:在并发较高的情况下,可能会出现多个客户端同时创建临时节点的情况。这时可以使用 `CyclicBarrier` 或者 `CountDownLatch` 进行同步,确保每次只有一个客户端创建锁节点。 4. 异常情况的处理:在进行锁操作时,需要处理各种异常情况,比如连接断开、网络超时等,保证系统的稳定性和可靠性。 综上所述,基于 ZooKeeper 可以实现 Java 分布式锁,通过创建临时有序节点和监听上一个节点的删除来实现锁的竞争和获取。 ### 回答3: 实现基于 ZooKeeperJava 分布式锁可以遵循以下步骤: 1. 连接 ZooKeeper:首先,通过 Java API 连接到 ZooKeeper 服务器,可以使用 zookeeper API 提供的 ZooKeeper 类来创建一个连接对象。 2. 创建锁节点:在 ZooKeeper 上创建一个父节点作为锁的根节点,该节点的所有子节点都作为锁节点。可以使用 zookeeper API 的 create() 方法创建临时顺序节点。 3. 获取锁:每个需要获取锁的进程都要通过创建一个临时顺序节点来竞争锁。通过 zookeeper API 的 getChildren() 方法获取锁根节点的所有子节点,如果创建的节点序号是当前所有节点中最小的,则表示获取到了锁。 4. 监听锁节点变化:如果未能获取到锁,应该在创建节点后,使用 zookeeper API 的 exists() 方法注册一个监听器来监听创建的子节点。当监听到创建的子节点发生变化时,判断自己的节点是否变成了最小的节点,如果是则表示获取到了锁。 5. 释放锁:对于已经获取到锁的进程,执行完任务后,需要通过 zookeeper API 的 delete() 方法将自己创建的锁节点删除,这样其他进程就能获取到该锁了。 需要注意的是,在分布式环境下,网络通信可能会出现延迟或故障,因此需要考虑到这些情况来保证分布式锁的正确性和可靠性。此外,还需考虑到异常情况处理、死锁检测和容错等问题,以确保分布式锁的高可用性和可靠性。 以上是使用 ZooKeeper 实现 Java 分布式锁的基本步骤,通过合理地使用 ZooKeeper 的 API,可以轻松实现分布式环境下的锁机制。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值