Zookeeper入门(3)——Leader选举与分布式锁

Zookeeper客户端创建

  • zkClient:ZK客户端的基本配置和创建
@Configuration
@ConfigurationProperties(prefix = "zookeeper")
@Getter
@Setter
@ToString
public class ZkProp {
    @Value("address")
    private String address;

    @Value("maxRetries")
    private String maxRetries;

    @Value("sleepTimeMs")
    private String sleepTimeMs;

    @Value("leaderPath")
    private String leaderPath;

}

@Getter
@Setter
@ToString
@Configuration
public class ZkConfiguration {

    @Resource
    private ZkProp zkProp;

    @Bean
    public CuratorFramework getCuratorFramework() throws InterruptedException {
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(Integer.parseInt(zkProp.getSleepTimeMs()),
            Integer.parseInt(zkProp.getMaxRetries()));
        CuratorFramework client = CuratorFrameworkFactory.newClient(zkProp.getAddress(), retryPolicy);
        client.start();
        client.blockUntilConnected();
        return client;
    }
}
  • ZkRepository:封装基本的创建节点,判断节点是否存在等操作
@Getter
@Setter
@Repository
public class ZkRepository {
    @Resource
    private CuratorFramework zkClient;

    public boolean isExits(String path) throws Exception {
        return zkClient.checkExists().forPath(path) != null;
    }

    public String createPersistPath(String path) throws Exception {
        return zkClient.create().creatingParentsIfNeeded().forPath(path);
    }

    public String createPersistPathWithData(String path, String data) throws Exception {
        return zkClient.create().creatingParentsIfNeeded().forPath(path, data.getBytes(StandardCharsets.UTF_8));
    }

    public String createEphemeralPath(String path) throws Exception {
        return zkClient.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(path);
    }

    public String createEphemeralPathWithData(String path, String data) throws Exception {
        return zkClient.create()
            .creatingParentsIfNeeded()
            .withMode(CreateMode.EPHEMERAL)
            .forPath(path, data.getBytes(StandardCharsets.UTF_8));
    }
}

一、Leader选举

1、获取领导权后不更换:

  • LeaderLatch:选主类

  • LeaderLatchListener:选主watch监听

使用场景:在多个节点选出一个主节点去执行任务,leader当选会触发事件,Follower竞争失败也会触发事件

使用示例
@Slf4j
@Service
public class LeaderLatchService {
    @Resource
    private ZkRepository zkRepository;

    public LeaderLatch electLeader(String electPath, LeaderLatchListener listener) throws Exception {
        // 判断选主路径是否不存在
        if (zkRepository.isExits(electPath)) {
            log.error("elect leader failed, the elect path is exits!");
            return null;
        }
        // 根据选主路径创建LeaderLatch
        LeaderLatch latch = new LeaderLatch(zkRepository.getZkClient(), electPath);
        // 添加监听器
        latch.addListener(listener);
        // 开始选主
        latch.start();
        return latch;
    }

    public static class AppLeaderLatchListener implements LeaderLatchListener {

        // 如果当选leader,调用此方法
        @Override
        public void isLeader() {
            log.info("I am leader, thread name is {}.", Thread.currentThread().getName());
        }

        // 如果未当选leader,调用此方法
        @Override
        public void notLeader() {
            log.info("Failed to elect leader");
        }
    }

}
测试

创建一个线程池,提交5个线程去选主,其中只有一个线程当选leader

public static void leaderElection(ConfigurableApplicationContext run) throws ExecutionException, InterruptedException {
    ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
    taskExecutor.setCorePoolSize(10);
    taskExecutor.setMaxPoolSize(20);
    taskExecutor.initialize();
    List<Future<String>> futureList = new ArrayList<>();
    for (int i = 0; i < 5; i++) {
        int threadNum = i;
        Callable<String> callable = () -> {
            Thread.currentThread().setName("Thread" + threadNum);
            int sleep = (int) (Math.random() * 10);
            System.out.println(Thread.currentThread().getName() + " sleep Time is " + sleep);
            TimeUnit.SECONDS.sleep(sleep);
            LeaderLatchService latchService = run.getBean(LeaderLatchService.class);
            LeaderLatchService.AppLeaderLatchListener listener = new LeaderLatchService.AppLeaderLatchListener();
            LeaderLatch latch = latchService.electLeader("/numb/elect", listener);
            TimeUnit.SECONDS.sleep(2);
            if (latch != null && latch.hasLeadership()) {
                return Thread.currentThread().getName() + ", i am leader";
            }
            return Thread.currentThread().getName() + ", i am follower";
        };
        Future<String> submit = taskExecutor.submit(callable);
        futureList.add(submit);
    }
    for (Future<String> future : futureList) {
        System.out.println(future.get());
    }
}

2、感知领导权的变化:

  • LeaderSelector:利用Curator中InterProcessMutex分布式锁进行抢主,抢到锁的即为Leader。主要有两个方法

    //开始抢主
    void start()
    //在抢到leader权限并释放后,自动加入抢主队列,重新抢主
    void autoRequeue()
    
  • LeaderSelectorListenerLeaderSelectorListenerLeaderSelector客户端节点成为Leader后回调的一个监听器,在takeLeadership()回调方法中编写获得Leader权利后的业务处理逻辑

    //抢主成功后的回调,执行完后自动释放leader权限
    void takeLeadership()
    
  • LeaderSelectorListenerAdapterLeaderSelectorListenerAdapter是实现了LeaderSelectorListener接口的一个抽象类,封装了客户端与zk服务器连接挂起或者断开时的处理逻辑(抛出抢主失败CancelLeadershipException),一般监听器推荐实现该类

  • CancelLeadershipException:抢主失败时抛出异常

@Service
public class LeaderSelectorService extends LeaderSelectorListenerAdapter implements Closeable {

    private LeaderSelector leaderSelector;


    @Override
    public void close() throws IOException {

    }

    @Override
    public void takeLeadership(CuratorFramework client) throws Exception {
        int sleep = (int) (Math.random() * 10);
        System.out.println(Thread.currentThread().getName() + " sleep Time is " + sleep);
        TimeUnit.SECONDS.sleep(sleep);
        System.out.println(Thread.currentThread().getName() + " i am leader");
    }
}

@SpringBootApplication
public class CuratorDemoApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(CuratorDemoApplication.class, args);
        leaderSelect(run);
    }
    public static void leaderSelect(ConfigurableApplicationContext run) {
        final String leaderPath = "/numb/select";
        CuratorFramework zkClient = run.getBean(CuratorFramework.class);
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(10);
        taskExecutor.setMaxPoolSize(20);
        taskExecutor.initialize();
        for (int i = 0; i < 5; i++) {
            LeaderSelectorService leaderSelectorService = new LeaderSelectorService();
            LeaderSelector leaderSelector = new LeaderSelector(zkClient, leaderPath, leaderSelectorService);
            leaderSelector.autoRequeue();
            leaderSelector.start();
        }
        while (true);
    }
}

二、分布式锁

1、可重入独占锁InterProcessMutex

InterProcessMutex实现原理

InterProcessMutex通过在zookeeper的某路径节点下创建临时序列节点来实现分布式锁,即每个线程(跨进程的线程)获取同一把锁前,都需要在同样的路径下创建一个节点,节点名字由uuid + 递增序列组成。而通过对比自身的序列数是否在所有子节点的第一位,来判断是否成功获取到了锁。当获取锁失败时,它会添加watcher来监听前一个节点的变动情况,然后进行等待状态。直到watcher的事件生效将自己唤醒,或者超时时间异常返回。

InterProcessMutex常用方法
// 获取锁,获取不到阻塞
public void acquire() throws Exception
// 带超时机制的获取锁
public boolean acquire(long time, TimeUnit unit) throws Exception
// 释放锁
public void release() throws Exception
使用示例
public class InterProcessMutexService {

    private static final String MUTEX_PATH = "/numb/inter_process_mutex";

    @Resource
    private CuratorFramework zkClient;

    public void mutex() {
        InterProcessMutex lock1 = new InterProcessMutex(zkClient, MUTEX_PATH);
        InterProcessMutex lock2 = new InterProcessMutex(zkClient, MUTEX_PATH);

        Thread thread1 = new Thread(() -> {
            try {
                lock1.acquire();
                System.out.println("thread1 get lock");
                lock1.acquire();
                System.out.println("thread1 get lock again");
                lock1.release();
                System.out.println("thread1 release lock");
                lock1.release();
                System.out.println("thread1 release lock again");
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        Thread thread2 = new Thread(() -> {
            try {
                lock2.acquire();
                System.out.println("thread2 get lock");
                lock2.acquire();
                System.out.println("thread2 get lock again");
                lock2.release();
                System.out.println("thread2 release lock");
                lock2.release();
                System.out.println("thread2 release lock again");
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        thread1.start();
        thread2.start();
    }

    public static void interProcessMutex(ApplicationContext application) {
        InterProcessMutexService bean = application.getBean(InterProcessMutexService.class);
        bean.mutex();
    }
}

2、读写锁InterProcessReadWriteLock

InterProcessReadWriteLock实现原理

​ 内部使用InterProcessMutex实现

InterProcessReadWriteLock使用方法
// 读锁
public InterProcessMutex readLock()
// 写锁
public InterProcessMutex writeLock()
使用示例
@Test
public void sharedReentrantReadWriteLock() throws Exception {
    // 创建读写锁对象, Curator 以公平锁的方式进行实现
    InterProcessReadWriteLock lock1 = new InterProcessReadWriteLock(client1, lockPath);
    // lock2 用于模拟其他客户端
    InterProcessReadWriteLock lock2 = new InterProcessReadWriteLock(client2, lockPath);

    // 使用 lock1 模拟读操作
    // 使用 lock2 模拟写操作
    // 获取读锁(使用 InterProcessMutex 实现, 所以是可以重入的)
    final InterProcessLock readLock = lock1.readLock();
    // 获取写锁(使用 InterProcessMutex 实现, 所以是可以重入的)
    final InterProcessLock writeLock = lock2.writeLock();

    /**
	  * 读写锁测试对象
	  */
    class ReadWriteLockTest {
        // 测试数据变更字段
        private Integer testData = 0;
        private Set<Thread> threadSet = new HashSet<>();

        // 写入数据
        private void write() throws Exception {
            writeLock.acquire();
            try {
                Thread.sleep(10);
                testData++;
                System.out.println("写入数据 \t" + testData);
            } finally {
                writeLock.release();
            }
        }

        // 读取数据
        private void read() throws Exception {
            readLock.acquire();
            try {
                Thread.sleep(10);
                System.out.println("读取数据 \t" + testData);
            } finally {
                readLock.release();
            }
        }

        // 等待线程结束, 防止 test 方法调用完成后, 当前线程直接退出, 导致控制台无法输出信息
        public void waitThread() throws InterruptedException {
            for (Thread thread : threadSet) {
                thread.join();
            }
        }

        // 创建线程方法
        private void createThread(final int type) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        if (type == 1) {
                            write();
                        } else {
                            read();
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
            threadSet.add(thread);
            thread.start();
        }

        // 测试方法
        public void test() {
            for (int i = 0; i < 5; i++) {
                createThread(1);
            }
            for (int i = 0; i < 5; i++) {
                createThread(2);
            }
        }
    }

    ReadWriteLockTest readWriteLockTest = new ReadWriteLockTest();
    readWriteLockTest.test();
    readWriteLockTest.waitThread();
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值