一、ACM输入输出
1、模拟栈的输出
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、构造链表
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、删除重复元素
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、构造二叉树
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?
- lua 脚本需要整合这个redis操作,以一种事务方式执行,效率会低一些。
- redis decr + setnx 后置的非独占竞争的乐观锁,可以提高吞吐量。
三、项目问题(AI)
1、MQ如何实现消息有序性
• 方案设计:
- 单Partition + 单消费者:确保用户事件按顺序处理。
- 事务消息:通过Kafka事务消息保证库存扣减与事件发布的原子性。
- 事件溯源:记录用户中奖事件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抽象类,每个过滤节点(如BlacklistFilter、WeightFilter)实现doFilter方法并调用下一个节点。 -
异常回滚:
- 使用
try-catch捕获过滤异常,通过标记事务回滚。 - 日志记录异常原因(如用户黑名单拒绝),返回友好提示(“因参与次数过多,暂无法抽奖”)。
- 使用
• 场景举例:用户抽奖时,先校验黑名单→再验证权重→最后扣减库存,任一环节失败则终止流程并补偿。
3. 离散化概率空间优化
问题:如何通过离散化概率空间将抽奖概率计算的复杂度从O(n)优化到O(1)?请结合代码示例说明。
- 问题分析:原始概率数组(如[10%,20%,70%])需遍历计算随机数落点(O(n)),在百万级请求下会成为瓶颈。
原始方案直接遍历所有奖品计算累计概率(O(n)),优化步骤如下:
-
离散化概率:将连续概率区间转化为离散的整数范围。例如,奖品A概率30%(0-30)、奖品B概率70%(31-100);
-
哈希映射构建:生成
Map<Integer, Prize>,键为累计概率阈值,值为对应奖品; -
快速查询:通过
Math.random() * 100生成随机数,用TreeMap.floorKey()在O(log n)时间找到归属奖品。
4. 高并发库存扣减
问题:如何通过Redis分段锁和数据库异步更新实现高并发库存扣减?请说明具体方案及防超卖机制。
-
Redis方案:
- 分段消费:将大库存拆分为多个Redis键(如
lottery:prize:1:stock),用户请求随机命中其中一个键执行DECR操作,降低单键热点。 - 锁机制:使用
SETNX实现分布式锁(lockKey = "lock:prize:1"),锁超时时间设为500ms,防止死锁。
- 分段消费:将大库存拆分为多个Redis键(如
-
数据库异步更新:
- 用户中奖后,通过
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的典型应用场景有哪些
2、Zookeeper的数据结构是怎么样的?
3、Zookeeper集群中的角色有哪些?有什么区别?
4、Zookeeper是CP的还是AP的?
5、Zookeeper是选举机制是怎样的?
1. 选举场景
6、什么是脑裂?如何解决?
7、如何用Zookeeper实现分布式锁?
8、怎样使用Zookeeper实现服务发现?
9、Zookeeper的缺点有哪些?
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的某些实现。
- 示例:ZooKeeper原生
11、Zookeeper的持久节点与持久有序节点应用场景
ZooKeeper的持久节点(Persistent Node)和持久有序节点(Persistent Sequential Node)在分布式系统中承担不同角色,其应用场景主要根据数据特性和业务需求决定:
1. 持久节点(Persistent Node)
特性
• 创建后永久存在,直到显式删除。
• 数据不依赖客户端会话,即使客户端断开,节点仍然保留。
• 无顺序性,节点名称由用户自定义。
典型应用场景
-
服务注册与发现
• 服务启动时在固定路径(如/services/user-service)下创建持久节点,存储服务地址等信息。其他服务通过监听该路径获取可用服务列表。• 优势:即使服务宕机,节点仍保留,避免因临时断开导致服务误删。
-
配置中心
• 存储全局静态配置(如数据库连接参数),节点名称自定义(如/config/database)。客户端监听节点变化实现动态更新。 -
全局唯一ID生成
• 在固定路径下直接创建节点(如/ids/order_id),节点数据存储递增的ID值。适用于无需顺序保证的简单ID生成。 -
元数据存储
• 存储分布式系统的元信息(如集群节点状态、权限信息),需长期持久化。
2. 持久有序节点(Persistent Sequential Node)
特性
• 继承持久节点的持久化特性。
• 自动附加递增序号(如 0000000001),序号全局唯一且按创建顺序分配。
典型应用场景
- 分布式任务队列
• 任务按顺序创建有序节点(如/tasks/task_0000000001),消费者按序号顺序处理任务,确保FIFO执行。 - 分布式锁的公平性实现
• 客户端通过创建有序节点(如/locks/lock_)竞争锁,序号最小的节点获得锁。其他客户端监听前序节点的删除事件,实现公平锁。 - 有序数据处理
• 如日志记录、事件通知等需按生产顺序处理的场景。例如,消息队列中每条消息对应一个有序节点,消费者按序号消费。 - 资源分配
• 按顺序分配资源(如IP地址、设备号)。例如,创建/resources/ip_有序节点,序号直接作为分配标识。
3. 核心区别与选型建议
| 特性 | 持久节点 | 持久有序节点 |
|---|---|---|
| 顺序性 | 无 | 自动递增序号 |
| 适用场景 | 数据无序、长期存储 | 需严格顺序或公平性保证的场景 |
| 节点命名 | 用户自定义 | 系统自动生成带序号的名称 |
| 典型用例 | 服务注册、配置中心 | 分布式锁、任务队列、资源分配 |
12、Zookeeper的watch机制是如何工作的?
13、Zookeeper是如何保证创建的节点是唯一的?
14、Zookeeper的缺点有哪些?
五、语雀-分库分表面试题
1、什么是分库?分表?分库分表
1. 分库
2. 分表
2、分表算法都有哪些?
1. 一致性哈希
3、分库分表后会带来哪些问题?
4、在分库分表时,如果遇到了对商品名称的模糊查询,要怎么处理?
✅在分库分表时,如果遇到了对商品名称的模糊查询,要怎么处理?