分布式架构-ZK客户端工具Curator框架分布式锁及基本使用

分布式架构-基于Curator分布式锁及基本使用

一、Curator

Curator是Netflix公司开源的一套zookeeper客户端框架,解决了很多Zookeeper客户端非常底层的细节开发工作,包括连接重连、反复注册Watcher和NodeExistsException异常等等。Patrixck Hunt(Zookeeper)以一句“Guava is to Java that Curator to Zookeeper”给Curator予高度评价。

Curator官网 http://curator.apache.org/

二、Curator分布式锁实现方案

Curator已经实现分布式锁的核心部分,其中关于分布式锁的核心API:

InterProcessMutex:分布式可重入排它锁
InterProcessSemaphoreMutex:分布式排它锁
InterProcessReadWriteLock:分布式读写锁
InterProcessMultiLock:将多个锁作为单个实体管理的容器
获取锁:interProcessMutex.acquire()
释放锁:interProcessMutex.release();

引入依赖:

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

引入依赖需要注意curator和Zk版本对应问题:
curator3和4 支持 zk3.5,
curator2 支持 zk3.4 和 3.5。

分布式锁基本使用:

 InterProcessMutex interProcessMutex = null;
    try {
        interProcessMutex = new InterProcessMutex(curatorFramework, lockPath);
        interProcessMutex.acquire();
        log.info("<获取锁成功....>");
     
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (interProcessMutex != null){
         log.info("<释放锁....>");
          interProcessMutex.release();
        }
    }

Curator关于分布式锁的api挺明了的,如果想使用zk自己实现分布式锁,也可以参考下面写我写的一篇博客。

https://blog.csdn.net/qq_43692950/article/details/112409116

下面就说下如何基于Curator简单封装成项目开发中使用的样子。为了方便调用将它封装在了AOP中,切入点为自定义注解,方便统一调用:

在这里插入图片描述
其中自动意注解为:
在这里插入图片描述
第一个参数为是否包含事物,如果有的话,会自动在方法执行完提交事物或异常回滚,
因为在Curator的分布式锁解决方案中,提供了线程等待超时的设置,但没有持有锁超时的设置,如果某个线程长时间的持有锁不释放,其他线程便处于一直等待中,因此在获得锁的时候将锁的信息存在了缓存中,在定时任务中判断持有锁是否超时,超时则中断线程释放锁资源,如果含有事物,则通过设置事物的超时和锁的超时时间一致(注意:Spring 的事物超时时间是针对SQL的执行时间,比如设置事物超时为5s,先插入数据库后sleep 10s,是不会回滚的,而反过来先sleep 后执行sql则会回滚,所以如果不确定哪个可能执行时间较长,可以在方法最后写一个固定的查询或其他db操作语句来延长事物时间),这样锁超时中断时事物也会回滚。第二个便是一次等待锁超时时间,在封装中获取锁超时直接抛出异常终止程序执行。后两个参数是时间单位和超时重试次数。满足超时时间和重复获取次数才算获取锁超时。
先将Curator注入到Spring容器中:

 	@Bean
    public CuratorFramework curator(){
        // 重试策略
        // 初始休眠时间为 1000ms, 最大重试次数为 3
        log.info("Curator初始化");
        RetryPolicy retry = new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries);
        // 创建一个客户端, 60000(ms)为 session 超时时间, 15000(ms)为连接超时时间
        CuratorFramework client = CuratorFrameworkFactory.newClient(zkUrl, sessionTimeoutMs, connectionTimeoutMs, retry);
        client.start();
        log.info("Curator初始化成功");
        return client;
    }

下面是我AOP的封装

@Component
@Slf4j
@Aspect
public class ExtAsyncAop {
    @Autowired
    CuratorFramework curatorFramework;

    private String lockPath = "/lock";

    @Autowired
    private TransactionUtils transactionUtils;

    @Pointcut("@annotation(com.bxc.zkcuratordemo.LockFz.annotation.BxcZkLock)")
    public void BrokerAspect(){
    }

    @AfterThrowing(value = "BrokerAspect()",throwing = "e")
    public void doAfterThrowingGame(JoinPoint jp, Exception e) {
        String name = jp.getSignature().getName();
        System.out.println(name+"方法异常通知:"+e.toString());
    }


    @Around("BrokerAspect()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        BxcZkLock declaredAnnotation = ((MethodSignature) pjp.getSignature()).getMethod().getDeclaredAnnotation(BxcZkLock.class);
        TransactionStatus transactionStatus = null;
        InterProcessMutex interProcessMutex = new InterProcessMutex(curatorFramework,lockPath);
        int count = declaredAnnotation.retriesNum();
        long timeOut = declaredAnnotation.timeOut();
        TimeUnit t = declaredAnnotation.t();
        try {
            int i = 0;
            while (i < count){
                boolean acquire = interProcessMutex.acquire(timeOut, t);
                if (acquire){
                    log.info("Thread:"+Thread.currentThread().getId()+" 获取锁成功 ");
                    break;
                }
                i++;
            }
            if (i >= count){
                throw new Exception("获取锁失败");
            }
        } catch (Exception e) {
            log.error("获取锁失败 = "+e.toString());
            throw e;
        }

        String serviceId = UUID.randomUUID().toString();
        if (isTransaction(declaredAnnotation)){
            transactionStatus = transactionUtils.begin();
            LockScheduled.lockInfoMap.put(serviceId, new LockInfo(serviceId, Thread.currentThread(), LockStatusEnum.START,interProcessMutex,System.currentTimeMillis()));
        }else {
            LockScheduled.lockInfoMap.put(serviceId, new LockInfo(serviceId, Thread.currentThread(), LockStatusEnum.START,interProcessMutex,System.currentTimeMillis()));
        }
        try{
            Object obj = pjp.proceed();
            System.out.println("方法执行结束");
            if (transactionStatus != null){
                transactionUtils.commit(transactionStatus);
            }
            interProcessMutex.release();
            LockScheduled.lockInfoMap.remove(serviceId);
            return obj;
        }catch (Exception e){
            if (isTransaction(declaredAnnotation)){
                transactionUtils.rollback(transactionStatus);
            }
            interProcessMutex.release();
            LockScheduled.lockInfoMap.remove(serviceId);
            throw e;
        }
    }


    public boolean isTransaction(JoinPoint pjp){
        BxcZkLock declaredAnnotation = ((MethodSignature) pjp.getSignature()).getMethod().getDeclaredAnnotation(BxcZkLock.class);
        if (LockStatusEnum.Translation == declaredAnnotation.translation()){
            return true;
        }
        return false;
    }

    public boolean isTransaction(BxcZkLock bxcZkLock){
        if (LockStatusEnum.Translation == bxcZkLock.translation()){
            return true;
        }
        return false;
    }
}

在Aop的环绕通知中,默认使用的可重入锁,当然这个也可以在注解中再加个变量判断使用哪种锁,首先拿到自定义注解的参数,然后开了一个while循环,目的就是为了重复获取锁,如果一直没获取到锁,肯定i>=count,在这个时候抛出异常,停止下面的执行,如果获取到锁,则将锁的信息,如果开启事物也会将事物对象一并存入Map中,交给定时任务。下面为定时任务的逻辑。

@Slf4j
@Component
public class LockScheduled {
	//锁的超时时间为5秒(可配置配置文件中)
    public Long locktimeout = 5000L;


    public static Map<String, LockInfo> lockInfoMap = new ConcurrentHashMap<>();


    /**
     * 检测超时未释放的锁
     */
    @Scheduled(cron = "0/2 * * * * *")
    public void taskService() {
        try {
            lockInfoMap.forEach((k, lockInfo) -> {
                log.info("分布式锁超时检测");
                if ((System.currentTimeMillis() - lockInfo.getCreateTime()) >= locktimeout) {
                    log.info("持有锁超时关闭-----> " + lockInfo.getLockId());
                    try {
                        lockInfo.getZkLock().release();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                     //直接停止线程,事物超过时间没有提交会自动回滚
                    lockInfo.getLockThread().interrupt();
                    //移除队列
                    lockInfoMap.remove(k);
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在定时任务中,判断锁的创建时间是否大于秒如果大于,则将锁释放,有事物也会回滚事物,并强制停止线程,其实此时还需要优化,应该将信息存到其他某个地方,人工补偿此次数据。

三、Curator的基本使用

学习了上面的分布式锁,其实Curator还是个不错的zk客户端工具,附带学下一些其他的api吧:

  1. 创建一个初始内容为空的持久节点

      client.create().forPath(path);
    
  2. 创建一个包含内容的持久节点

    client.create().forPath(path,"aaa".getBytes());
    
  3. 创建临时节点,并递归创建父节点,递归创建父节点时,父节点为持久节点。

    client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(path);
    
  4. 删除一个子节点

    client.delete().forPath(path);
    
  5. 删除节点并递归删除其子节点

    client.delete().deletingChildrenIfNeeded().forPath(path);
    
  6. 指定版本进行删除如果此版本已经不存在,则抛出异常

    client.delete().withVersion(1).forPath(path);
    
  7. 强制保证删除一个节点

    client.delete().guaranteed().forPath(path);
    
  8. 普通查询

    client.getData().forPath(path);
    
  9. 包含状态查询

     Stat stat = new Stat();
     client.getData().storingStatIn(stat).forPath(path);
    
  10. 普通更新

    client.setData().forPath(path,"bbb".getBytes());
    
  11. 指定版本更新

     client.setData().withVersion(1).forPath(path);
    
  12. 监听事件

byte[] content = client.getData().usingWatcher(new Watcher() {
     @SneakyThrows
     @Override
     public void process(WatchedEvent watchedEvent) {
         System.out.println("监听器watchedEvent:" + watchedEvent);
         System.out.println(new String(client.getData().forPath(path)));
     }
 }).forPath(path);
 System.out.println("监听节点内容:" + new String(content));

或者:

NodeCache cache = new NodeCache(client, path, false);
cache.start(true);
cache.getListenable().addListener(new NodeCacheListener() {
    @Override
    public void nodeChanged() throws Exception {
        System.out.println("Node data update, new data: " + new String(cache.getCurrentData().getData()));
    }
});
  1. 监听子节点动作事件
String path = "/bxc";
PathChildrenCache cache = new PathChildrenCache(client, path, true);
cache.start(PathChildrenCache.StartMode.POST_INITIALIZED_EVENT);
cache.getListenable().addListener(new PathChildrenCacheListener(){
   public void childEvent(CuratorFramework client, PathChildrenCacheEvent event)  throws Exception{
       switch(event.getType()) {
           case CHILD_ADDED :
               System.out.println("添加节点" + event.getData().getPath());
               break;

           case CHILD_UPDATED :
               System.out.println("修改节点" + event.getData().getPath());
               break;

           case CHILD_REMOVED :
               System.out.println("删除节点," + event.getData().getPath());
               break;
           default:
               break;
       }
   }
});

下面造作均会触发上面监听:

client.create().withMode(CreateMode.PERSISTENT).forPath(path+"/tem");
client.setData().forPath(path+"/tem","1111".getBytes());
client.delete().forPath(path+"/tem");
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小毕超

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值