文章目录
1、服务器动态上下线
1.1、需求
某分布式系统中,主节点可以有多台,可以动态上下线,任意一台客户端都能实时感知到主节点服务器的上下线。
1.2、需求分析
1.3、具体实现
-
先在集群上创建 /servers 节点
[zk: localhost:2181(CONNECTED) 3] create /servers "servers" Created /servers
-
在 Idea 中创建包名:zkcase1
-
服务器端向 Zookeeper 注册代码
public class DistributeServer { public static String connectString = "192.168.196.128:2181,192.168.196.129:2181,192.168.196.130:2181"; private static final int sessionTimeout = 20000; private ZooKeeper zkClient = null; private String parentNode = "/servers"; public void getConnect() throws IOException { zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() { @Override public void process(WatchedEvent watchedEvent) { } }); } //注册服务器 public void registServer(String hostname) throws InterruptedException, KeeperException { String create = zkClient.create(parentNode + "/server", hostname.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); System.out.println(hostname + " is online " + create); } //业务功能 public void business(String hostname) throws InterruptedException { System.out.println(hostname + " is working ..."); Thread.sleep(Long.MAX_VALUE); } public static void main(String[] args) throws InterruptedException, KeeperException, IOException { //1、获取zk连接 DistributeServer server = new DistributeServer(); server.getConnect(); //2、利用zk连接注册服务器信息 server.registServer(args[0]); //3、开启业务功能 server.business(args[0]); } }
-
客户端代码
public class DistributeClient { public static String connectString = "192.168.196.128:2181,192.168.196.129:2181,192.168.196.130:2181"; private static final int sessionTimeout = 20000; private ZooKeeper zkClient = null; private String parentNode = "/servers"; //创建到zk的客户端连接 public void getConnection() throws IOException { zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() { @Override public void process(WatchedEvent watchedEvent) { //再次启动监听 try { getServerList(); } catch (InterruptedException e) { e.printStackTrace(); } catch (KeeperException e) { e.printStackTrace(); } } }); } //获取服务器列表信息 private void getServerList() throws InterruptedException, KeeperException { //1、获取服务器节点信息,并且对父节点进行监听 List<String> children = zkClient.getChildren(parentNode, true); //2、存储服务器信息列表 ArrayList<String> servers = new ArrayList<>(); //3、遍历所有节点,获取节点中的主机名称信息 for (String child : children) { byte[] data = zkClient.getData(parentNode + "/" + child, false, null); servers.add(new String(data)); } //4、打印服务器列表信息 System.out.println(servers); } //业务功能 public void business() throws InterruptedException { System.out.println("client is working ..."); Thread.sleep(Long.MAX_VALUE); } public static void main(String[] args) throws InterruptedException, KeeperException, IOException { //1、获取zk连接 DistributeClient client = new DistributeClient(); client.getConnection(); //2、获取servers的子节点信息,从中获取服务器信息列表 client.getServerList(); //3、业务进程启动 client.business(); } }
1.4、测试
-
在 Linux 命令行上操作增加减少服务器
-
启动 DistributeClient 客户端
-
在 hadoop102 上 zk 的客户端 /servers 目录上创建临时带序号节点
[zk: localhost:2181(CONNECTED) 4] create -e -s /servers/hadoop102 "hadoop102" Created /servers/hadoop1020000000000 [zk: localhost:2181(CONNECTED) 5] create -e -s /servers/hadoop103 "hadoop103" Created /servers/hadoop1030000000001
-
观察 Idea 控制台变化
[hadoop102, hadoop103]
-
执行删除操作
[zk: localhost:2181(CONNECTED) 6] delete /servers/hadoop1030000000001
-
观察 Idea 控制台变化
[hadoop102]
-
-
在 Idea 上操作增加减少服务器
-
启动 DistributeClient 客户端(如果已经启动过,不需要重启)
-
启动 DistributeServer 服务
点击 Edit Configurations…
在弹出的窗口中(Program arguments)输入想启动的主机,例如,hadoop102
回到 DistributeServer 的 main 方法 ,右键 ,在弹出的窗口中点击 Run “DistributeServer.main()”
观察 DistributeServer 控制台,提示 hadoop102 is working
观察 DistributeClient 控制台,提示 hadoop102 已经上线
-
2、ZooKeeper 分布式锁案例
什么叫做分布式锁呢?
比如说 “进程 1” 在使用该资源的时候,会先去获得锁,“进程 1” 获得锁以后会对该资源保持独占,这样其他进程就无法访问该资源,“进程 1” 用完该资源以后就将锁释放掉,让其他进程来获得锁,那么通过这个锁机制,我们就能保证了分布式系统中多个进程能够有序的访问该临界资源。那么我们把这个分布式环境下的这个锁叫作分布式锁。
2.1、原生 Zookeeper 实现分布式锁案例
-
分布式锁实现
package lock; import org.apache.zookeeper.CreateMode; import org.apache.zookeeper.ZooDefs; import org.apache.zookeeper.ZooKeeper; import java.util.Collections; import java.util.List; import java.util.concurrent.locks.LockSupport; public class DistributedLock { private ZooKeeper zkClient; ThreadLocal<String> threadLocal = new ThreadLocal<>(); public DistributedLock() { try { String connectString = "192.168.196.128:2181,192.168.196.129:2181,192.168.196.130:2181"; int sessionTimeout = 20000; zkClient = new ZooKeeper(connectString, sessionTimeout, null); // 判断节点 /exclusive_lock 是否存在 if (zkClient.exists("/exclusive_lock", false) == null) { // 不存在则创建节点 zkClient.create("/exclusive_lock", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); } } catch (Exception e) { e.printStackTrace(); } } public void lock() { try { // 创建对应的临时带序号节点 String currentLockNode = zkClient.create("/exclusive_lock/" + "seq-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); // 判断创建的节点是否是最小的序号节点,如果是获取到锁;如果不是,监听他序号前一个节点 List<String> children = zkClient.getChildren("/exclusive_lock", false); // 如果children 只有一个值,那就直接获取锁;如果有多个节点,需要判断谁最小 if (children.size() > 1) { Collections.sort(children); // 获取节点名称 seq-00000000 String thisNode = currentLockNode.substring("/exclusive_lock/".length()); // 通过 seq-00000000 获取该节点在children集合的位置 int index = children.indexOf(thisNode); /** * 因为在zkClient.create和zkClient.getChildren("/exclusive_lock", false);可能有其它线程也创建了节点, * 所以并不是说只有 children.size() == 1 这个线程才是第一个创建节点的线程 */ if (index == 0) {// 如果自己就是第一个节点,那么获得锁, System.out.println(Thread.currentThread().getName() + "获得锁"); threadLocal.set(currentLockNode); return; } // String preNode = "/exclusive_lock/" + children.get(index - 1); Thread thread = Thread.currentThread(); // 监听它前一个节点的变化,如果前一个节点删除了,会调用回调函数把自己唤醒 zkClient.getData(preNode, watchedEvent -> LockSupport.unpark(thread), null); // 把自己挂起 LockSupport.park(); } threadLocal.set(currentLockNode); System.out.println(Thread.currentThread().getName() + "获得锁"); } catch (Exception e) { e.printStackTrace(); } } public void unlock() { try { System.out.println(Thread.currentThread().getName() + "释放了锁"); zkClient.delete(threadLocal.get(), -1); } catch (Exception e) { e.printStackTrace(); } } }
-
分布式锁测试
-
创建两个线程
public class DistributedLockTest { public static void main(String[] args) { DistributedLock lock01 = new DistributedLock(); Runnable task01 = () -> { lock01.lock(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } lock01.unlock(); }; DistributedLock lock02 = new DistributedLock(); Runnable task02 = () -> { lock02.lock(); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } lock02.unlock(); }; for (int i = 0; i < 10; i++) { new Thread(task01, "server01-thread-" + i).start(); new Thread(task02, "server02-thread-" + i).start(); } } }
-
观察控制台变化:
server02-thread-0获得锁 server02-thread-0释放了锁 server02-thread-2获得锁 server02-thread-2释放了锁 server02-thread-1获得锁 server02-thread-1释放了锁 server02-thread-3获得锁 server02-thread-3释放了锁 server02-thread-4获得锁 server02-thread-4释放了锁
-
2.2、Curator 框架实现分布式锁案例
-
原生的 Java API 开发存在的问题
- 会话连接是异步的,需要自己去处理。比如使用 CountDownLatch
- Watch 需要重复注册,不然就不能生效
- 开发的复杂性还是比较高的
- 不支持多节点删除和创建。需要自己去递归
-
Curator 是一个专门解决分布式锁的框架,解决了原生 Java API 开发分布式遇到的问题
详情请查看官方文档: https://curator.apache.org/index.html
-
Curator 案例实操
-
添加依赖
<!-- https://mvnrepository.com/artifact/org.apache.curator/curator-framework --> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>5.2.1</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.curator/curator-recipes --> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>5.2.1</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.curator/curator-client --> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-client</artifactId> <version>5.2.1</version> </dependency>
-
代码实现
public class CuratorLockTest { private String rootNode = "/locks"; // zookeeper server 列表 String connectString = "192.168.196.128:2181,192.168.196.129:2181,192.168.196.130:2181"; // session超时时间 private int sessionTimeout = 20000; //connect超时时间 private int connectTimeout = 20000; public static void main(String[] args) { new CuratorLockTest().test(); } //测试 private void test() { //创建分布式锁 1 final InterProcessMutex lock1 = new InterProcessMutex(getCuratorFramework(), rootNode); //创建分布式锁 2 final InterProcessMutex lock2 = new InterProcessMutex(getCuratorFramework(), rootNode); new Thread(new Runnable() { @Override public void run() { try { lock1.acquire(); System.out.println("线程1获取锁"); //测试锁重入 lock1.acquire(); System.out.println("线程1再次获取锁"); Thread.sleep(5 * 1000); lock1.release(); System.out.println("线程1释放锁"); lock1.release(); System.out.println("线程1再次释放锁"); } catch (Exception e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { @Override public void run() { try { lock2.acquire(); System.out.println("线程2获取锁"); //测试锁重入 lock2.acquire(); System.out.println("线程2再次获取锁"); Thread.sleep(5 * 1000); lock2.release(); System.out.println("线程2释放锁"); lock2.release(); System.out.println("线程2再次释放锁"); } catch (Exception e) { e.printStackTrace(); } } }).start(); } // 分布式锁初始化 private CuratorFramework getCuratorFramework() { //重试策略,初试时间 3 秒,重试 3 次 RetryPolicy policy = new ExponentialBackoffRetry(3000, 3); //通过工厂创建 Curator CuratorFramework client = CuratorFrameworkFactory.builder() .connectString(connectString) .connectionTimeoutMs(connectTimeout) .sessionTimeoutMs(sessionTimeout) .retryPolicy(policy).build(); //开启连接 client.start(); System.out.println("zookeeper 初始化完成..."); return client; } }
-
观察控制台变化
zookeeper 初始化完成... 线程2获取锁 线程2再次获取锁 线程2释放锁 线程2再次释放锁 线程1获取锁 线程1再次获取锁 线程1释放锁 线程1再次释放锁
-
3、企业面试真题
3.1、选举机制
半数机制,超过半数的投票通过,即通过。
-
第一次启动选举规则:
投票过半数时, 服务器 id 大的胜出
-
第二次启动选举规则:
- EPOCH 大的直接胜出
- EPOCH 相同,事务 id 大的胜出
- 事务 id 相同,服务器 id 大的胜出
3.2、生产集群安装多少 zk 合适?
安装奇数台。
生产经验:
- 10 台服务器:3 台 zk;
- 20 台服务器:5 台 zk;
- 100 台服务器:11 台 zk;
- 200 台服务器:11 台 zk。
服务器台数多:好处,提高可靠性;坏处:提高通信延时。
3.3、常用命令
ls、get、create、delete