博客记录-day122-ACM输入输出+项目问题+Zookeeper,分库分表面试题

114 阅读26分钟

一、ACM输入输出

1、模拟栈的输出

image.png

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            int n = scanner.nextInt();
            if (n == 0) break;
            int[] ans = new int[n];
            for (int i = 0; i < n; i++) {
                ans[i] = scanner.nextInt();
            }
            //System.out.println(Arrays.toString(ans));
            ArrayDeque<Integer> stack = new ArrayDeque<>();
            int point = 0;
            for (int i = 1; i <= n; i++) {
                stack.push(i);
                while (!stack.isEmpty() && stack.peek() == ans[point]) {
                    stack.pop();
                    point++;
                }
            }
            if (stack.isEmpty()) System.out.println("Yes");
            else System.out.println("No");
        }
    }
}

2、构造链表

image.png

import java.util.LinkedList;
import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in); // 创建Scanner对象用于输入读取
        
        LinkedList list = new LinkedList<>(); // 声明链表对象
        String[] s = sc.nextLine().split(" "); // 读取初始化数据
        list = init(s); // 调用初始化方法构造链表
        
        int num = Integer.parseInt(sc.nextLine()); // 读取需要执行的命令数量
        int initLine = 0; // 命令执行计数器
        
        
        // 循环处理命令直到达到指定数量或输入结束
        while(sc.hasNextLine()){
            String[] ss = sc.nextLine().split(" ");
            switch (ss[0]){ // 根据命令类型分发处理
                case "get": // 获取元素操作
                    // 参数验证:检查索引有效性
                    System.out.println(list.isEmpty()?"get fail":list.get(Integer.parseInt(ss[1]) - 1));
                    break;
                    
                case "delete": // 删除元素操作
                    int del_pos = Integer.parseInt(ss[1]);
                    // 边界检查:删除位置必须有效
                    if(del_pos > list.size())
                        System.out.println("delete fail");
                    else {
                        // 元素前移算法:将pos及之后的元素整体左移
                        for (int i = del_pos; i < list.size(); i++) {
                            list.set(i-1, list.get(i));
                        }
                        list.removeLast(); // 删除最后一个元素(原目标位置)
                        System.out.println("delete OK");
                    }
                    break;
                    
                case "insert": // 插入元素操作
                    int pos = Integer.parseInt(ss[1]);
                    int data = Integer.parseInt(ss[2]);
                    // 插入位置有效性验证
                    if(pos -1 > list.size()) // 转换为0-based索引验证
                        System.out.println("insert fail");
                    else if (list.isEmpty()) { // 空链表特殊处理
                        list.add(data);
                        System.out.println("insert OK");
                    } else {
                        // 标准插入算法:
                        // 1. 保存末尾元素(用于填充新位置后的空缺)
                        list.add(list.getLast());
                        // 2. 从倒数第二个元素开始前移
                        for (int i = list.size()-2; i-1 >= pos-1 ; i--) {
                            list.set(i, list.get(i-1)); // 元素右移
                        }
                        list.set(pos-1, data); // 在目标位置插入新元素
                        System.out.println("insert OK");
                    }
                    break;
                    
                case "show": // 展示链表内容
                    if(list.isEmpty()){
                        System.out.println("Link list is empty");
                    }else {
                        // 构建格式化输出字符串
                        StringBuilder sb = new StringBuilder();
                        for (int i = 0; i < list.size(); i++) {
                            sb.append(list.get(i));
                            if(i==list.size()-1) sb.append("\n"); // 换行
                            else sb.append(" "); // 空格分隔
                        }
                        System.out.print(sb.toString());
                    }
                    break;
                default: 
                    break;
            }
            if(initLine++ == num) break; // 命令计数器递增并检查终止条件
        }
        sc.close(); // 关闭Scanner释放资源

    }

    /**
     * 初始化链表(头插法)
     * @param s 初始化参数数组(格式:n a1 a2 ... an)
     * @return 构建完成的链表对象
     */
    private static LinkedList init(String[] s) {
        LinkedList list = new LinkedList<>();
        // 从第二个元素开始处理(第一个元素是数量)
        for (int i = 1; i < s.length ; i++) {
            list.addFirst(Integer.parseInt(s[i])); // 头插法构造链表
        }
        return list;
    }
}

3、删除重复元素

image.png

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            int n = scanner.nextInt();
            if (n == 0) {
                System.out.println("list is empty");
                break;
            }
            int[] ans = new int[n];
            for (int i = 0; i < n; i++) {
                ans[i] = scanner.nextInt();
            }
            for (int i = 0; i < n; i++) {
                System.out.print(ans[i]);
                if (i == n - 1) {
                    System.out.println();
                } else {
                    System.out.print(" ");
                }
            }
            int left = 0, right = 0;
            while (right < n) {
                if (right > 0 && ans[right] == ans[right - 1]) {
                    right++;
                } else {
                    ans[left] = ans[right];
                    left++;
                    right++;
                }
            }
            for (int i = 0; i < left; i++) {
                System.out.print(ans[i]);
                if (i == left) {
                    System.out.print("\n");
                } else {
                    System.out.print(" ");
                }
            }
            System.out.println();
        }
    }
}

4、构造二叉树

image.png

import java.util.HashMap;
import java.util.Scanner;

public class Main {
    private static HashMap<Character, Integer> hashmap;
    private static int nodeIdx;

    public static class TreeNode {
        char val;
        TreeNode left;
        TreeNode right;

        public TreeNode() {};

        public TreeNode(char val) {
            this.val = val;
        }
    }

    // 需要哈希表快速定位 中序序列中结点->下标的映射
    private static TreeNode buildTree(char[] preOrder, char[] inOrder) {
        int n = preOrder.length;
        return build(preOrder, inOrder, 0, n - 1); // 最后两个参数控制inOrder的范围
    }

    private static TreeNode build(char[] preOrder, char[] inOrder, int l, int r) {
        int n = preOrder.length;
        if (nodeIdx >= n || l > r) return null;
        char rootVal = preOrder[nodeIdx];
        TreeNode root = new TreeNode(rootVal);
        ++nodeIdx;
        int mid = hashmap.get(rootVal);
        root.left = build(preOrder, inOrder, l, mid - 1);
        root.right = build(preOrder, inOrder, mid + 1, r);
        return root;
    }

    private static void dfs(TreeNode root) {
        if (root == null) return;
        dfs(root.left);
        dfs(root.right);
        System.out.print(root.val);
    }

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        while (in.hasNextLine()) {
            String[] input = in.nextLine().split(" ");
            char[] preOrder = input[0].toCharArray();
            char[] inOrder = input[1].toCharArray();
            hashmap = new HashMap<Character, Integer>();
            nodeIdx = 0;
            for (int i = 0; i < inOrder.length; i++) {
                hashmap.put(inOrder[i], i);
            }
            TreeNode root = buildTree(preOrder, inOrder);
            dfs(root);
            System.out.println("");
        }
    }
}

二、项目问题

1、我看你简历有提到,把抽奖划分为抽奖前、中、后,三个动作。请具体结合场景讲解下,为什么这样设计

这个的设计得益于在 Spring/MyBatis 框架源码的学习,在源码中经常会出现对一个流程进行拆分解耦,流程可扩展的点,如 Spring 是 Bean 对象的拆解,MyBatis 是会话流程的拆解。所以在设计大营销的抽奖模块时,对于需求中的各类功能点;黑名单抽奖、权重抽奖、默认抽奖、抽奖N次解锁、兜底抽奖等等情况,是可以拆解为抽奖前、中、后,3个行为动作的,基于这样的考虑后,就可以设计出非常容易扩展的松耦合结构。

2、什么场景下使用了责任链模式,什么场景使用了组合模式,为什么?

在设计完抽奖前、中、后,搜耦合的结构模型后,对于抽奖前要执行哪种抽奖,但单向选择问题。所以这里使用了责任链模式,进行节点流程判断,从黑名单、权重,最后到默认,走一个单独的具体抽奖,所以使用责任链更为合适。

之后是进入抽奖的中和后,这两部的流程是相对复杂的,需要判断用户抽奖了几次,对于不同次会限定是否能获得某个奖品,同时还有库存的扣减,如果库存不足或者不满足n次抽奖得到某个奖品,则会进行兜底。那么这就是一个树规则的交叉流程,所以会使用了组合模式构建一颗规则树,并通过数据库表的动态配置决定在抽奖前完成后,后续的流程要如何进行。

3、抽奖也是一种瞬时峰值很高的业务场景,那么对于抽中奖品后的库存扣减是怎么做的?

关于库存的扣减,是一个非常重要的流程。尤其是这种单独资源竞争的场景,如果设计的不好,很容易把服务打挂。

所以在这套系统设计中,为了避免库存扣减直接更新库表的行级锁,而导致大量的用户进行等待状态。所以把数据库表的库存同步到 Redis 缓存中,在通过 incr 扣减的方式进行消费,同时为了确保在临界状态、库存恢复、异常处理等情况下不超卖,而对每一条产生从 incr 值,与抽奖的策略ID组合一个key,进行 setnx 加锁兜底,来保证不超卖。—— 这样的设计是颗粒度更小的锁方案设计,性能接近于无锁化。

4、到库存的扣减是通过 Redis 滑块锁实现,那么最终同步库是怎么做的,怎么降低对数据库的压力的?

关于 redis 缓存和数据库表库存数据的流程,设计了异步更新,保持最终一致性的设计。在执行完库存的扣减操作后(在抽奖中规则树库存节点流程),发送一个扣减完成到 Redis 的异步队列(可以使用MQ+延迟消费),之后通过定时 Schedule Job 来消费队列。这样就可以控制效率速率,降低对数据库的压力。(因为我们不能 Redis 扣减的多快,就直接打到库表上,那样对数据库的压力依然很大,容易打挂)

5、你提到了接口的单一职责设计,这部分具体讲解下。

单一职责原则的核心思想是,一个类应该只有一个引起它变化的原因。也就是说一个类应该只负责一项任务或功能,如果一个类承担了过多的职责,那么这个类就会变得复杂,难以维护和扩展。

这样的原则在一些需要长期使用、迭代、维护的功能设计上,是非常重要的。我们要尽可能的让大营销的抽奖领域领域模块具备独立性,所以要使用单一职责原则。在这个原则约束下,设计了3个接口类;抽奖策略接口、奖品信息接口、库存处理接口(异步扣减等),这样3个接口的设计,在将来需要扩展的时候,会非常容易。(可能会问具体编码,问的比较多样性,这部分需要自己阅读代码来学习)

6、在项目中你提到了可以支持不同场景的抽奖诉求,比如;多少积分后可以抽奖一个固定范围的奖品,或者抽奖n次后,才可以中奖某个奖品。这部分你是怎么做的?库表怎么设计的?

这块的流程,就是前面关于大营销抽奖领域模型的设计,从而确定的库表设计。也就是常说的领域->驱动设计。

库表包括;策略表、策略明细(库存、概率、规则key)、奖品表、规则表、规则树(3个表)—— 这部分在学习项目后,需要具备能在纸上画出库表ER图。

7、抽奖接口响应时间是多少?

这样的问题主要考察你是否做了项目的上线,以及了解过接口的响应时间。如果做过就非常好回答,没做过乱说是挺容易被继续提问的。

参考数据;2c2g 云服务器,部署项目(含mysql、redis),占用63%内存,抽奖接口响应时间为38~55毫秒(项目有完整的手把手部署教程,还有监控部署教程,可以自己部署验证)。

8、做项目中,什么问题难住你的时间最长,为什么?

这是一个开放问题,重点考察你对项目的开发中个人的积累。你可以针对自己的学习过程中,有哪个流程的实现,让你最为有感触,即可回答。

如;可以对大营销抽奖模型流程的设计和库表设计,最为耗时,因为你不断的在思考如何拆解出一个好扩展的松耦合结构,同时拆解后,还要保证搜耦合下的高内聚。所以这部分是比较耗时的。同时也可以说在设定某个方法的,名称、入参、出参时,做了大量的思考。因为名字的定义非常影响以后的理解。好的代码就是文档,所以对于这样的东西花费不少时间。

9、为什么使用分库分表,做了分库分表数据聚合查询如何处理?

首先我们要知道,是前期就做好分库分表方案,还是后期在做系统重构分库分表和数据迁移哪个成本高。显然是后面的成本更高。

而互联网中大厂中,分库分表的架构设计都是非常熟练的,因为有成熟方案,所以前期就分库分表了。但,为了节省服务器空间。所以把分库分表的库,用服务器虚拟出来机器安装。这样即不过多的占用服务器资源,也方便后续数据量真的上来了,好拆分。

那么这里的分库分表后的数据怎么提供汇总、聚合的查询呢?

这里需要用到阿里的 canal 组件,基于 mysql 的 binlog 日志,把自己伪装为一个从数据库,通过 dump 交换完成数据的接收和处理。最终把数据同步到 Elasticsearch 等文件服务中在提供聚合查询。对于需要实时的查询以及数据的处理,还可以用到 Flink 方式进行流式计算。

10、抽奖奖品库存如何处理,怎么保证最终一致性?

在抽奖秒杀这样的场景下,都需要把库存缓存到 Redis 中进行使用。而不能数据库表加行级锁,否则大量的秒杀进行通过加锁和等待释放,就会夯住数据库链接直至拖垮整个服务。

那么使用缓存通过大营销项目中的颗粒度更低的分段锁后,怎么来保证一致性呢。这里需要3个步骤,首先是每次扣减完库存,都会写入到 Redis 延迟队列 / MQ 延迟消息,缓慢更新数据库库存。之后是 Redis 内的预热库存消耗完毕后,发送最终 MQ 消息,更新数据库的剩余库存为 0,最终活动结束后,还有任务补偿,扫描抽奖所产生的的参与记录单,更新最终的库存消耗。这里就可以用订单 MQ 通过 Flink 计算,更新最终库存也是可以的。

11、写入中奖记录,发送MQ消息失败如何处理?

本身发送MQ是可能存在万分之一或者十万分之的失败的,而数据库操作和MQ操作,本身不能做数据库事务。但又要保证失败后的补偿处理。所以要结合中奖记录在写一条发送MQ的任务记录,任务记录上有一个状态,标记是否发送完成,这样就可以通过任务扫描的方式完成 MQ 的补偿发送。

但这里要知道,做完本身的中奖记录和任务记录后写库事务后,要顺序的可以是多线程的方式,完成一次MQ发送,并且更新数据库 Task 记录。这里是为了业务流程最快的推进,如果是更新失败也没关系,还有兜底的任务补偿。【任务补偿的数量并不多,但非常需要这个手段】

12、生产者可能多次发送同一个MQ,怎么保证奖品不会超发?

这是一个幂等的设计处理,MQ 的消息是必须含带具有唯一标识的业务ID的。比如订单ID、奖品ID、支付单ID、交易单ID、贷款单ID等等。接收MQ的系统,通过唯一ID业务,更新或者写库的时候可以保证幂等性。这样也就不会产生超发的可能。

13、抽奖算法如何提供O(1)时间复杂度,提高抽奖效率?

在大营销系统中,运营人员配置好抽奖活动后,开始上线对外后,会进行数据的预热数据。这个预热的过程会把活动信息、策略信息、库存信息都存储到 Redis 里进行使用。

而抽奖的策略就是记录了一个策略下N个奖品的概率,将概率转换为对应的整数数量,写入到缓存中。那么在抽奖的时候就按照整数数量生成随机数来抽奖。这样用空间换时间的效率是非常高的。

14、为啥使用Redisdecr分段消费+setnx锁兜底而不使用lua?

  1. lua 脚本需要整合这个redis操作,以一种事务方式执行,效率会低一些。
  2. redis decr + setnx 后置的非独占竞争的乐观锁,可以提高吞吐量。

三、项目问题(AI)

1、MQ如何实现消息有序性

方案设计

  1. 单Partition + 单消费者:确保用户事件按顺序处理。
  2. 事务消息:通过Kafka事务消息保证库存扣减与事件发布的原子性。
  3. 事件溯源:记录用户中奖事件ID,消费端按ID排序处理。

在RabbitMQ中实现消息有序性需围绕生产、传输、消费三阶段设计:生产端采用单线程或序列号(sequence_id)控制消息入队顺序,结合直连交换机(Direct Exchange)确保路由一致性;传输层依赖队列的FIFO特性,避免使用广播类交换机,并通过持久化配置防丢;消费端以单消费者模式(或分片队列+消费者组)结合手动ACK保证顺序处理,失败消息通过独立重试队列按序重试。需权衡性能与一致性,例如分片提升吞吐但引入分区顺序限制,最终通过监控sequence_id校验和异常处理保障全局有序。适用于电商订单、日志流水等强顺序场景,但需接受分布式系统中跨集群无法绝对保序的局限性。

1. 分片队列+消费组

在分布式系统中,分片队列 + 消费者组 是提升消息处理性能与可靠性的核心设计模式。分片队列通过将大消息队列按规则(如哈希路由键、Sequence ID或一致性哈希算法)拆分为多个子队列,实现水平扩展和负载均衡,每个子队列独立存储消息,避免单点瓶颈;消费者组则由一组消费者组成,每个消费者负责处理特定分片的消息,通过动态分配分片任务实现并行处理和容错——当某个消费者宕机时,其他消费者可接管其分片,保障服务连续性。

设计上,分片策略需结合业务场景选择:例如电商订单处理中,可基于订单ID哈希分片到多个队列,消费者组监听所有分片,每个消费者专注一个队列以保证局部顺序性;若需全局顺序,可结合全局唯一的Sequence ID,由消费者组按ID范围分配任务并校验处理顺序。实战中,需权衡吞吐量与顺序性:分片越多并行度越高,但跨分片处理可能引入乱序,因此需通过序列号校验或业务逻辑容忍局部有序,适用于日志收集等场景;而强一致性场景(如支付流水)则建议单队列+单消费者模式。

最佳实践包括:选择高基数字段(如用户ID)作为分片键,避免热点;利用一致性哈希算法动态调整分片分布,适应弹性扩缩容;通过监控分片处理延迟和消息堆积量,动态优化分片数量与消费者规模;同时启用生产者确认、手动ACK和自动恢复机制,防丢保序。总结来看,分片队列+消费者组通过“分而治之”的策略,在分布式环境下平衡了高性能与可靠性,是处理高并发、大规模消息流的核心架构模式,但需根据业务对顺序性和扩展性的需求,灵活设计分片规则与消费逻辑。

2、抽奖策略系统(DDD/高并发/数据库设计方向)

1. 领域驱动设计(DDD)

问题:抽奖系统的核心领域模型如何划分?请说明限界上下文(BC)的边界设计逻辑,并举例说明如何通过DDD解决业务复杂性问题?

回答参考

  • 核心领域模型:将抽奖系统划分为用户领域(处理身份验证、黑名单)、奖品领域(管理奖品库存、规则配置)、抽奖领域(核心业务流程,如抽奖资格校验、结果计算)。

  • 限界上下文划分

    • 用户服务BC:处理用户登录、黑名单管理(独立部署,避免与其他模块耦合)。
    • 奖品服务BC:封装奖品库存、中奖概率配置(高并发读写,单独扩展)。
    • 抽奖服务BC:整合用户、奖品数据,执行抽奖逻辑(核心事务边界)。

2. 责任链模式应用

问题:抽奖前置过滤链(如黑名单校验、权重过滤)如何通过责任链模式实现?若某个节点失败如何保证流程回滚?
回答参考

  • 链式设计:定义FilterChain接口和AbstractFilter抽象类,每个过滤节点(如BlacklistFilterWeightFilter)实现doFilter方法并调用下一个节点。

  • 异常回滚

    • 使用try-catch捕获过滤异常,通过标记事务回滚。
    • 日志记录异常原因(如用户黑名单拒绝),返回友好提示(“因参与次数过多,暂无法抽奖”)。

场景举例:用户抽奖时,先校验黑名单→再验证权重→最后扣减库存,任一环节失败则终止流程并补偿。


3. 离散化概率空间优化

问题:如何通过离散化概率空间将抽奖概率计算的复杂度从O(n)优化到O(1)?请结合代码示例说明。

  • 问题分析:原始概率数组(如[10%,20%,70%])需遍历计算随机数落点(O(n)),在百万级请求下会成为瓶颈。

原始方案直接遍历所有奖品计算累计概率(O(n)),优化步骤如下:

  1. 离散化概率:将连续概率区间转化为离散的整数范围。例如,奖品A概率30%(0-30)、奖品B概率70%(31-100);

  2. 哈希映射构建:生成Map<Integer, Prize>,键为累计概率阈值,值为对应奖品;

  3. 快速查询:通过Math.random() * 100生成随机数,用TreeMap.floorKey()在O(log n)时间找到归属奖品。


4. 高并发库存扣减

问题:如何通过Redis分段锁和数据库异步更新实现高并发库存扣减?请说明具体方案及防超卖机制。

  • Redis方案

    • 分段消费:将大库存拆分为多个Redis键(如lottery:prize:1:stock),用户请求随机命中其中一个键执行DECR操作,降低单键热点。
    • 锁机制:使用SETNX实现分布式锁(lockKey = "lock:prize:1"),锁超时时间设为500ms,防止死锁。
  • 数据库异步更新

    • 用户中奖后,通过RocketMQ发送消息到库存补偿队列,消费者异步执行UPDATE prize SET stock = stock - 1 WHERE id = ?
    • 防重复消费:消息携带唯一流水号,消费者通过Redis记录已处理流水号,避免重复扣减。

5. RabbitMQ补偿机制

问题:如何保证RabbitMQ消息的最终一致性?若MQ宕机导致消息丢失,如何兜底?

  • 可靠性设计

    • 生产者:启用confirm模式,发送消息后等待Broker确认,未收到ACK则重试。
    • 消费者:手动提交@TransactionalEventListener监听消息处理结果,失败时触发补偿逻辑。
  • 兜底方案

    • 若MQ宕机,通过定时任务扫描Redis中奖记录(标记为“已处理”但未入库),批量补全数据库。
    • 数据一致性:使用CAS操作(Redis原子性自增)确保库存最终一致。

3、DB-Router数据库路由组件

1. 动态数据源切换

问题:基于AbstractRoutingDataSource实现动态数据源时,如何保证线程安全?请说明具体实现细节。

  • 线程安全实现:继承AbstractRoutingDataSource,重写determineCurrentLookupKey()方法,通过ThreadLocal存储当前数据源标识(如user_id%10)。

  • 线程安全风险:确保ThreadLocal在任务完成后清理(通过try-finally@After注解),避免线程池复用导致数据源泄漏。


8. 哈希路由算法

问题:如何设计哈希路由算法以避免数据倾斜?请结合具体代码说明扰动函数的作用。

回答参考

  • 扰动函数作用:对分片字段(如用户ID)进行哈希处理(如MurmurHash3),打散分布均匀性,避免数据倾斜。

  • 代码优化

    public int getShard(String userId) {
        long hash = MurmurHash3.hash64(userId.getBytes());
        return (int) (hash % shardCount + 1); // +1避免0分片
    }
    
  • 数据倾斜应对:结合一致性哈希算法,动态增删节点时仅迁移部分数据。


9. AOP拦截SQL改写

问题:如何通过AOP动态解析路由字段并改写SQL?请举例说明具体实现。

回答参考

  • AOP实现:定义注解@DbRoute,通过@Around拦截方法,解析注解参数(如路由字段user_id)。

  • SQL改写示例

    @Aspect
    public class DbRouteAspect {
        @Around("@annotation(DbRoute)")
        public Object route(ProceedingJoinPoint pjp) throws Throwable {
            String routeField = ...; // 获取注解参数
            String value = ...; // 动态获取字段值(如通过反射)
            DynamicDataSource.setDataSourceKey(value);
            return pjp.proceed();
        }
    }
    
  • SQL兼容性:支持MyBatis动态SQL(如<if test="routeField != null">AND user_id = #{routeField}</if>)。


四、语雀-Zookeeper面试题

1、Zookeeper的典型应用场景有哪些

✅Zookeeper的典型应用场景有哪些?

image.png

2、Zookeeper的数据结构是怎么样的?

✅Zookeeper的数据结构是怎么样的?

image.png

3、Zookeeper集群中的角色有哪些?有什么区别?

✅Zookeeper集群中的角色有哪些?有什么区别?

image.png

4、Zookeeper是CP的还是AP的?

✅Zookeeper是CP的还是AP的?

image.png

5、Zookeeper是选举机制是怎样的?

✅Zookeeper是选举机制是怎样的?

image.png

1. 选举场景

image.png

6、什么是脑裂?如何解决?

✅什么是脑裂?如何解决?

image.png

7、如何用Zookeeper实现分布式锁?

✅如何用Zookeeper实现分布式锁?

image.png

8、怎样使用Zookeeper实现服务发现?

✅怎样使用Zookeeper实现服务发现?

image.png

9、Zookeeper的缺点有哪些?

✅Zookeeper的缺点有哪些?

image.png

10、Zookeeper的临时节点与临时有序节点应用场景

在ZooKeeper及其应用中,​​临时节点(Ephemeral)​​和​​临时有序节点(Ephemeral Sequential)​​的使用场景主要取决于是否需要​​顺序性保证​​以及具体的业务需求。以下是典型场景的对比:


​1. 使用临时节点(Ephemeral)的场景​

​特点​​:

  • ​生命周期​​:与客户端会话绑定,会话结束(如断开或超时)时节点自动删除。
  • ​无顺序性​​:节点名称由客户端指定,无自增序号。

​典型应用​​:

  • ​服务发现​
    服务实例注册临时节点,节点名称可以是服务标识(如/service-instance-1)。当服务下线时,节点自动删除,其他服务可感知到该实例不可用。

    • ​示例​​:微服务架构中,服务实例动态注册与发现。
  • ​简单 Leader Election(选主)​
    多个客户端尝试创建同一个临时节点(如/leader)。由于ZooKeeper的原子性,只有一个客户端能成功创建节点,成为主节点。其他客户端监听该节点,若主节点消失,则重新选举。

    • ​示例​​:小型分布式系统的主节点选举,无需严格顺序。
  • ​状态同步​
    客户端通过临时节点存储临时状态(如会话状态),状态随会话终止而消失。


​2. 使用临时有序节点(Ephemeral Sequential)的场景​

​特点​​:

  • ​生命周期​​:与会话绑定,会话结束时节点删除。
  • ​顺序性​​:节点名称自动附加递增序号(如/lock-0000000001),全局唯一且有序。

​典型应用​​:

  • ​分布式锁(避免羊群效应)​
    客户端通过创建临时有序节点(如/locks/lock_前缀)申请锁。客户端获取所有子节点列表,判断自己是否是最小序号节点。若是,则获得锁;否则监听前一个节点的删除事件。释放锁时删除自身节点,触发后续节点竞争。

    • ​示例​​:数据库连接池管理、资源争用场景。
  • ​有序任务调度​
    任务按顺序分配给不同节点。例如,任务队列中的每个任务对应一个有序节点,工作节点按序号顺序处理任务。

    • ​示例​​:分布式任务队列(如Kafka消费者分区分配)。
  • ​公平选主(Ordered Leader Election)​
    需要严格按顺序选举主节点(如主从切换时保证数据一致性)。客户端通过创建有序临时节点,序号最小的节点成为主节点。

    • ​示例​​:ZooKeeper原生Leader Election的某些实现。

11、Zookeeper的持久节点与持久有序节点应用场景

ZooKeeper的持久节点(Persistent Node)和持久有序节点(Persistent Sequential Node)在分布式系统中承担不同角色,其应用场景主要根据数据特性和业务需求决定:


​1. 持久节点(Persistent Node)​
​特性​
• 创建后永久存在,直到显式删除。

• 数据不依赖客户端会话,即使客户端断开,节点仍然保留。

• 无顺序性,节点名称由用户自定义。

​典型应用场景​

  1. 服务注册与发现
    • 服务启动时在固定路径(如 /services/user-service)下创建持久节点,存储服务地址等信息。其他服务通过监听该路径获取可用服务列表。

    • 优势:即使服务宕机,节点仍保留,避免因临时断开导致服务误删。

  2. 配置中心
    • 存储全局静态配置(如数据库连接参数),节点名称自定义(如 /config/database)。客户端监听节点变化实现动态更新。

  3. 全局唯一ID生成
    • 在固定路径下直接创建节点(如 /ids/order_id),节点数据存储递增的ID值。适用于无需顺序保证的简单ID生成。

  4. 元数据存储
    • 存储分布式系统的元信息(如集群节点状态、权限信息),需长期持久化。


​2. 持久有序节点(Persistent Sequential Node)​
​特性​
• 继承持久节点的持久化特性。

• 自动附加递增序号(如 0000000001),序号全局唯一且按创建顺序分配。

​典型应用场景​

  1. 分布式任务队列
    • 任务按顺序创建有序节点(如 /tasks/task_0000000001),消费者按序号顺序处理任务,确保FIFO执行。
  2. 分布式锁的公平性实现
    • 客户端通过创建有序节点(如 /locks/lock_)竞争锁,序号最小的节点获得锁。其他客户端监听前序节点的删除事件,实现公平锁。
  3. 有序数据处理
    • 如日志记录、事件通知等需按生产顺序处理的场景。例如,消息队列中每条消息对应一个有序节点,消费者按序号消费。
  4. 资源分配
    • 按顺序分配资源(如IP地址、设备号)。例如,创建 /resources/ip_ 有序节点,序号直接作为分配标识。

​3. 核心区别与选型建议​

特性持久节点持久有序节点
顺序性自动递增序号
适用场景数据无序、长期存储需严格顺序或公平性保证的场景
节点命名用户自定义系统自动生成带序号的名称
典型用例服务注册、配置中心分布式锁、任务队列、资源分配

12、Zookeeper的watch机制是如何工作的?

✅Zookeeper的watch机制是如何工作的?

image.png

image.png

image.png

13、Zookeeper是如何保证创建的节点是唯一的?

✅Zookeeper是如何保证创建的节点是唯一的?

image.png

14、Zookeeper的缺点有哪些?

✅Zookeeper的缺点有哪些?

image.png

五、语雀-分库分表面试题

1、什么是分库?分表?分库分表

✅什么是分库?分表?分库分表?

1. 分库

image.png

2. 分表

image.png

2、分表算法都有哪些?

✅分表算法都有哪些?

image.png

image.png

1. 一致性哈希

image.png

image.png

3、分库分表后会带来哪些问题?

✅分库分表后会带来哪些问题?

image.png

4、在分库分表时,如果遇到了对商品名称的模糊查询,要怎么处理?

✅在分库分表时,如果遇到了对商品名称的模糊查询,要怎么处理?

image.png

5、分区和分表有什么区别?

✅分区和分表有什么区别?

image.png

image.png