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()
-
LeaderSelectorListener
:LeaderSelectorListener
是LeaderSelector
客户端节点成为Leader后回调的一个监听器,在takeLeadership()
回调方法中编写获得Leader权利后的业务处理逻辑//抢主成功后的回调,执行完后自动释放leader权限 void takeLeadership()
-
LeaderSelectorListenerAdapter
:LeaderSelectorListenerAdapter
是实现了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();
}