大数据领域面试题(数据中台方向) 本文档整理了大数据应用开发岗位的核心面试题,涵盖Java基础、大数据技术栈、OLAP数据库、数据仓库、中间件、项目经验等多个维度。每个类别按照基础问题 、一般问题 、困难问题 三个难度级别组织。
一、Java核心与后端框架 1.1 Java基础 1.1.1 集合框架 基础问题
Q1: ArrayList和LinkedList的区别?
A:
ArrayList:基于动态数组,随机访问O(1),插入删除O(n)
LinkedList:基于双向链表,随机访问O(n),插入删除O(1)
ArrayList适合查询多、修改少的场景;LinkedList适合频繁插入删除的场景
Q2: HashMap和Hashtable的区别?
A:
HashMap线程不安全,Hashtable线程安全(synchronized)
HashMap允许null键值,Hashtable不允许
HashMap效率更高,Hashtable已过时,推荐使用ConcurrentHashMap
Q3: HashSet的实现原理?
A:
HashSet内部使用HashMap实现
元素作为HashMap的key,value使用固定的Object对象
利用HashMap的key唯一性保证元素不重复
一般问题
Q1: HashMap的实现原理?
A: HashMap基于哈希表实现,使用数组+链表+红黑树(JDK1.8)的结构:
初始容量16,负载因子0.75
通过key的hashCode计算数组索引位置
当链表长度超过8且数组长度>=64时,链表转为红黑树
扩容时容量翻倍,重新计算hash分布
Q2: HashMap的扩容机制?
A:
当元素数量超过容量×负载因子时触发扩容
扩容时容量翻倍(2的幂次)
重新计算每个元素的hash值,分配到新数组
JDK1.8优化:扩容时保持链表顺序,减少重新hash的计算
Q3: ConcurrentHashMap如何保证线程安全?
A: JDK1.8采用分段锁+CAS机制:
使用Node数组+链表+红黑树结构
对数组元素加synchronized锁(只锁链表头或红黑树根节点)
使用CAS操作保证并发修改的安全性
支持并发读写,性能优于Hashtable
困难问题
Q1: ConcurrentHashMap在JDK1.7和JDK1.8的区别?
A:
JDK1.7 :使用Segment分段锁,每个Segment是一个独立的HashTable
JDK1.8 :取消Segment,使用CAS+synchronized,锁粒度更细
性能提升 :JDK1.8的并发性能更好,锁竞争更少
实现简化 :代码更简洁,维护更容易
Q2: HashMap为什么使用红黑树而不是AVL树?
A:
红黑树的插入删除操作更高效,旋转次数更少
红黑树对平衡性的要求更宽松,维护成本更低
在查找、插入、删除的综合性能上,红黑树更适合HashMap的场景
AVL树更适合读多写少的场景
Q3: 如何设计一个线程安全的LRU缓存?
A:
使用LinkedHashMap + ReentrantReadWriteLock
或者使用ConcurrentHashMap + 双向链表 + 读写锁
需要考虑并发读写、缓存淘汰策略
可以使用Caffeine或Guava Cache等成熟方案
1.1.2 多线程与并发编程 基础问题
Q1: 创建线程的方式有哪些?
A:
继承Thread类
实现Runnable接口
实现Callable接口(有返回值)
使用线程池(推荐)
Q2: synchronized关键字的作用?
A:
修饰实例方法:锁对象是当前实例
修饰静态方法:锁对象是当前类的Class对象
修饰代码块:锁对象是指定的对象
保证同一时刻只有一个线程能访问被锁定的代码
Q3: volatile关键字的作用?
A:
保证可见性:修改后立即刷新到主内存
禁止指令重排序:通过内存屏障实现
不保证原子性:如i++操作需要配合synchronized或AtomicInteger
一般问题
Q1: synchronized和ReentrantLock的区别?
A:
synchronized是JVM层面的锁,ReentrantLock是API层面的锁
ReentrantLock支持公平锁、可中断、多条件变量
synchronized发生异常自动释放锁,ReentrantLock需要在finally中手动释放
ReentrantLock提供更灵活的锁机制,但synchronized性能在JDK1.6优化后已接近
Q2: 线程池的核心参数有哪些?
A:
corePoolSize:核心线程数
maximumPoolSize:最大线程数
keepAliveTime:非核心线程空闲存活时间
workQueue:任务队列(ArrayBlockingQueue、LinkedBlockingQueue等)
threadFactory:线程工厂
handler:拒绝策略(AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy)
Q3: 线程池的拒绝策略?
A:
AbortPolicy:直接抛出异常(默认)
CallerRunsPolicy:调用者线程执行任务
DiscardPolicy:直接丢弃任务
DiscardOldestPolicy:丢弃队列中最老的任务,然后提交新任务
困难问题
Q1: AQS(AbstractQueuedSynchronizer)的原理?
A:
使用CLH队列(虚拟双向队列)管理等待线程
通过volatile int state表示同步状态
使用CAS操作保证原子性
支持独占锁和共享锁两种模式
ReentrantLock、CountDownLatch、Semaphore等都基于AQS实现
Q2: 如何实现一个自定义的线程池?
A:
需要实现任务队列、工作线程管理、任务提交、拒绝策略等
核心组件:BlockingQueue、Worker线程、ThreadFactory
需要考虑线程生命周期管理、异常处理、优雅关闭等
Q3: 死锁的产生条件和解决方案?
A:
产生条件 :互斥、请求与保持、不剥夺、循环等待
解决方案 :
避免嵌套锁
统一锁顺序
使用超时锁(tryLock)
死锁检测和恢复
Q4: CountDownLatch、CyclicBarrier、Semaphore的区别?
A:
CountDownLatch(倒计时门闩) :
一个或多个线程等待其他线程完成操作
计数器只能使用一次,不能重置
使用场景:等待多个线程完成后再执行后续操作
CyclicBarrier(循环屏障) :
多个线程互相等待,达到屏障点后继续执行
可以重复使用(cyclic)
使用场景:分阶段任务,需要所有线程到达某个阶段
Semaphore(信号量) :
控制同时访问资源的线程数量
可以设置许可数量
使用场景:限流、资源池管理
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 CountDownLatch latch = new CountDownLatch (3 );latch.await(); CyclicBarrier barrier = new CyclicBarrier (3 );barrier.await(); Semaphore semaphore = new Semaphore (5 );semaphore.acquire(); semaphore.release();
Q5: CompletableFuture的使用?
A:
CompletableFuture定义 :
Java 8引入的异步编程工具
支持链式调用和组合多个异步任务
比Future更强大,支持回调、组合等
常用方法 :
supplyAsync():异步执行有返回值的任务
runAsync():异步执行无返回值的任务
thenApply():任务完成后执行,有返回值
thenAccept():任务完成后执行,无返回值
thenCompose():组合两个CompletableFuture
thenCombine():合并两个CompletableFuture的结果
allOf():等待所有任务完成
anyOf():等待任意一个任务完成
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> { return "Hello" ; }); future.thenApply(s -> s + " World" ) .thenAccept(System.out::println); CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "Hello" ); CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "World" ); CompletableFuture<String> combined = f1.thenCombine(f2, (a, b) -> a + " " + b);
1.1.3 String与Object类 基础问题
Q1: String、StringBuilder、StringBuffer的区别?
A:
String :
不可变类,每次操作都创建新对象
线程安全(因为不可变)
适合字符串常量或少量字符串操作
StringBuilder :
可变类,内部使用char数组
线程不安全,性能高
适合单线程环境下的字符串拼接
StringBuffer :
可变类,内部使用char数组
线程安全(synchronized),性能略低于StringBuilder
适合多线程环境下的字符串拼接
性能对比 :
大量字符串拼接:StringBuilder > StringBuffer > String
少量字符串操作:差异不大
Q2: String的intern()方法?
A:
intern()作用 :
如果字符串常量池中存在该字符串,返回常量池中的引用
如果不存在,将字符串添加到常量池并返回引用
可以节省内存,但需要权衡性能
示例 :
1 2 3 4 5 String s1 = new String ("abc" );String s2 = s1.intern();String s3 = "abc" ;System.out.println(s1 == s2); System.out.println(s2 == s3);
使用场景 :
Q3: Object类的equals()和hashCode()方法?
A:
equals()方法 :
默认比较对象引用(==)
需要重写以实现值比较
重写equals()必须重写hashCode()
hashCode()方法 :
返回对象的哈希码
相等的对象必须有相同的hashCode
不等的对象尽量有不同的hashCode
重写规则 :
自反性:x.equals(x) == true
对称性:x.equals(y) == y.equals(x)
传递性:x.equals(y) && y.equals(z) => x.equals(z)
一致性:多次调用结果相同
非空性:x.equals(null) == false
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 @Override public boolean equals (Object obj) { if (this == obj) return true ; if (obj == null || getClass() != obj.getClass()) return false ; Person person = (Person) obj; return Objects.equals(name, person.name) && age == person.age; } @Override public int hashCode () { return Objects.hash(name, age); }
一般问题
Q1: String为什么是不可变的?
A:
不可变的原因 :
String类被final修饰,不能被继承
内部char数组被final修饰
没有提供修改char数组的方法
不可变的优势 :
线程安全 :多线程环境下可以安全共享
缓存优化 :可以缓存hashCode,提高性能
安全性 :作为参数传递时不会被修改
字符串常量池 :可以复用字符串,节省内存
不可变的缺点 :
大量字符串拼接性能差
需要使用StringBuilder或StringBuffer
Q2: ==和equals()的区别?
A:
==操作符 :
equals()方法 :
Object类中默认使用==比较
可以重写实现值比较
String类重写了equals(),比较字符串内容
示例 :
1 2 3 4 5 6 7 8 String s1 = new String ("abc" );String s2 = new String ("abc" );System.out.println(s1 == s2); System.out.println(s1.equals(s2)); String s3 = "abc" ;String s4 = "abc" ;System.out.println(s3 == s4);
困难问题
Q1: String的常量池机制?
A:
字符串常量池 :
JVM中专门存储字符串常量的区域
位于方法区(JDK1.7后移到堆)
可以复用字符串,节省内存
字符串创建方式 :
字面量 :String s = "abc",直接使用常量池
**new String()**:String s = new String("abc"),创建新对象
**intern()**:将字符串添加到常量池
内存优化 :
相同字面量共享同一个对象
使用intern()可以复用字符串
但需要注意性能开销
Q2: 如何正确重写equals()和hashCode()?
A:
重写equals()的步骤 :
检查引用是否相同
检查对象是否为null
检查类型是否相同
比较关键字段
重写hashCode()的原则 :
使用相同的字段计算hashCode
使用Objects.hash()简化代码
确保相等的对象有相同的hashCode
完整示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Person { private String name; private int age; @Override public boolean equals (Object obj) { if (this == obj) return true ; if (obj == null || getClass() != obj.getClass()) return false ; Person person = (Person) obj; return age == person.age && Objects.equals(name, person.name); } @Override public int hashCode () { return Objects.hash(name, age); } }
1.1.4 异常处理 基础问题
Q1: Java异常体系?
A:
Throwable :
Error:系统错误,如OutOfMemoryError
Exception:程序异常
RuntimeException:运行时异常,如NullPointerException
检查异常:编译时检查,如IOException
异常分类 :
检查异常 :必须处理(try-catch或throws)
运行时异常 :可以不处理
错误 :系统级错误,通常无法恢复
Q2: try-catch-finally的执行顺序?
A:
执行顺序 :
执行try块
如果发生异常,执行catch块
无论是否异常,都执行finally块
如果finally中有return,会覆盖try/catch中的return
示例 :
1 2 3 4 5 6 7 8 9 10 try { return 1 ; } catch (Exception e) { return 2 ; } finally { return 3 ; }
Q3: throw和throws的区别?
A:
throw :
在方法内部抛出异常
抛出的是异常对象
可以抛出任何Throwable或其子类
throws :
在方法声明中声明可能抛出的异常
告诉调用者需要处理这些异常
可以声明多个异常
示例 :
1 2 3 4 5 public void method () throws IOException { if (condition) { throw new IOException ("错误信息" ); } }
一般问题
Q1: 异常处理的最佳实践?
A:
实践原则 :
具体异常 :捕获具体异常,避免捕获Exception
不要忽略异常 :至少记录日志
资源管理 :使用try-with-resources自动关闭资源
异常转换 :将底层异常转换为业务异常
避免空的catch块 :至少要记录日志
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 try (FileInputStream fis = new FileInputStream ("file.txt" )) { } catch (FileNotFoundException e) { logger.error("文件未找到" , e); throw new BusinessException ("文件不存在" , e); } try { } catch (Exception e) { }
Q2: 自定义异常?
A:
创建自定义异常 :
继承Exception或RuntimeException
提供构造方法
可以添加自定义字段和方法
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class BusinessException extends RuntimeException { private String errorCode; public BusinessException (String message) { super (message); } public BusinessException (String errorCode, String message) { super (message); this .errorCode = errorCode; } public String getErrorCode () { return errorCode; } }
1.1.5 Java 8+新特性 基础问题
Q1: Lambda表达式?
A:
Lambda定义 :
函数式编程的语法糖
简化匿名内部类的写法
必须配合函数式接口使用
语法 :
1 2 3 4 5 6 (参数) -> { 方法体 } x -> x * 2 () -> System.out.println("Hello" )
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 List<String> list = Arrays.asList("a" , "b" , "c" ); Collections.sort(list, new Comparator <String>() { @Override public int compare (String a, String b) { return a.compareTo(b); } }); Collections.sort(list, (a, b) -> a.compareTo(b)); list.sort(String::compareTo);
Q2: Stream API的使用?
A:
Stream定义 :
对集合进行函数式操作
支持链式调用
惰性求值,终端操作时才执行
常用操作 :
中间操作 :filter、map、sorted、distinct、limit
终端操作 :forEach、collect、reduce、count、anyMatch
示例 :
1 2 3 4 5 List<String> result = list.stream() .filter(s -> s.length() > 3 ) .map(String::toUpperCase) .sorted() .collect(Collectors.toList());
Q3: Optional的使用?
A:
Optional定义 :
容器类,可能包含null值
避免NullPointerException
提供函数式编程风格
常用方法 :
of():创建非空Optional
ofNullable():创建可能为空的Optional
isPresent():判断是否有值
orElse():有值返回,无值返回默认值
orElseGet():有值返回,无值执行Supplier
map():转换值
flatMap():扁平化转换
示例 :
1 2 3 Optional<String> opt = Optional.ofNullable(str); String result = opt.orElse("default" );opt.ifPresent(System.out::println);
一般问题
Q1: 函数式接口?
A:
函数式接口定义 :
只有一个抽象方法的接口
可以用@FunctionalInterface注解标记
可以用Lambda表达式实现
常用函数式接口 :
Function<T, R>:接受T返回R
Consumer<T>:接受T无返回值
Supplier<T>:无参数返回T
Predicate<T>:接受T返回boolean
BiFunction<T, U, R>:接受T和U返回R
示例 :
1 2 3 4 Function<String, Integer> func = s -> s.length(); Consumer<String> consumer = s -> System.out.println(s); Supplier<String> supplier = () -> "Hello" ; Predicate<String> predicate = s -> s.length() > 5 ;
Q2: 方法引用?
A:
方法引用类型 :
静态方法引用 :类名::静态方法
实例方法引用 :对象::实例方法
类的实例方法引用 :类名::实例方法
构造器引用 :类名::new
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 list.sort(String::compareTo); String str = "hello" ;Supplier<Integer> len = str::length; Function<String, String> upper = String::toUpperCase; Supplier<List<String>> listSupplier = ArrayList::new ;
困难问题
Q1: Stream的并行流?
A:
并行流 :
使用parallelStream()创建并行流
利用多核CPU提高性能
需要注意线程安全
使用场景 :
注意事项 :
并行流不保证顺序
需要线程安全的数据结构
小数据量可能性能更差(线程切换开销)
示例 :
1 2 3 4 List<Integer> result = list.parallelStream() .filter(x -> x > 10 ) .map(x -> x * 2 ) .collect(Collectors.toList());
Q2: Java 8的时间API?
A:
新的时间API :
LocalDate:日期(年月日)
LocalTime:时间(时分秒)
LocalDateTime:日期时间
ZonedDateTime:带时区的日期时间
Instant:时间戳
Duration:时间间隔
Period:日期间隔
优势 :
示例 :
1 2 3 4 LocalDate date = LocalDate.now();LocalDateTime dateTime = LocalDateTime.now();Duration duration = Duration.between(start, end);Period period = Period.between(startDate, endDate);
1.1.6 JVM内存模型与GC调优 基础问题
Q1: JVM内存结构?
A:
堆(Heap):存放对象实例,分为新生代(Eden、Survivor0/1)和老年代
方法区(Method Area):存储类信息、常量、静态变量
程序计数器(PC Register):记录当前线程执行的字节码行号
虚拟机栈(VM Stack):存储局部变量表、操作数栈、方法出口
本地方法栈(Native Method Stack):为Native方法服务
Q2: 垃圾回收算法有哪些?
A:
标记-清除:标记需要回收的对象,统一清除;缺点:产生碎片
复制算法:将内存分为两块,每次使用一块,存活对象复制到另一块;适合新生代
标记-整理:标记后移动存活对象,避免碎片;适合老年代
分代收集:根据对象存活周期采用不同算法
Q3: 常见的垃圾回收器?
A:
Serial/Serial Old:单线程,适合小应用
ParNew:多线程版本的Serial,配合CMS使用
Parallel Scavenge/Old:吞吐量优先,适合后台任务
CMS:并发标记清除,低停顿,适合Web应用
G1:分代收集,可预测停顿时间,适合大内存应用
ZGC:超低延迟,适合超大堆内存
一般问题
Q1: 如何判断对象可以被回收?
A:
引用计数 :每个对象有引用计数,为0时可回收;无法解决循环引用
可达性分析 :从GC Roots开始,不可达的对象可回收
GC Roots :虚拟机栈中的引用、方法区静态变量、方法区常量、本地方法栈引用
Q2: 如何排查内存溢出问题?
A:
使用jmap生成堆转储文件:jmap -dump:format=b,file=heap.bin <pid>
使用jstat查看GC情况:jstat -gcutil <pid> 1000
使用MAT或VisualVM分析堆转储文件
检查是否有内存泄漏(对象无法被GC回收)
调整JVM参数:-Xmx、-Xms、-XX:NewRatio等
Q3: 新生代和老年代的比例?
A:
默认比例:新生代:老年代 = 1:2
新生代中Eden:Survivor0:Survivor1 = 8:1:1
可通过-XX:NewRatio调整比例
可通过-XX:SurvivorRatio调整Survivor比例
困难问题
Q1: G1垃圾回收器的工作原理?
A:
将堆内存划分为多个Region(1MB-32MB)
使用Remembered Set记录跨Region引用
并发标记阶段:标记存活对象
回收阶段:优先回收垃圾最多的Region
可预测停顿时间,适合大内存应用
Q2: 如何优化JVM参数?
A:
堆内存 :-Xmx、-Xms设置为相同值,避免动态调整
新生代 :根据对象生命周期调整-XX:NewRatio
GC选择 :根据应用特点选择G1、CMS等
GC日志 :开启-XX:+PrintGCDetails分析GC情况
内存泄漏 :使用-XX:+HeapDumpOnOutOfMemoryError自动生成dump
Q3: 如何分析GC日志?
A:
关注Full GC频率和耗时
关注Young GC频率和耗时
分析GC前后内存变化
找出GC频繁的原因(内存分配过快、对象生命周期长等)
使用GCViewer等工具可视化分析
1.1.7 反射与注解 基础问题
Q1: 反射是什么?
A:
反射定义 :
在运行时动态获取类的信息
可以创建对象、调用方法、访问字段
通过Class对象操作类
获取Class对象的方式 :
Class.forName("类名")
对象.getClass()
类名.class
示例 :
1 2 3 4 Class<?> clazz = Class.forName("com.example.Person" ); Object obj = clazz.newInstance();Method method = clazz.getMethod("getName" );String name = (String) method.invoke(obj);
Q2: 注解是什么?
A:
注解定义 :
元数据,提供程序信息
不影响程序执行
可以通过反射读取
常用注解 :
@Override:重写方法
@Deprecated:标记过时
@SuppressWarnings:抑制警告
@FunctionalInterface:函数式接口
@Retention:注解保留策略
@Target:注解作用目标
元注解 :
@Retention:SOURCE、CLASS、RUNTIME
@Target:TYPE、METHOD、FIELD等
@Documented:包含在JavaDoc中
@Inherited:可以继承
Q3: 自定义注解?
A:
创建注解 :
1 2 3 4 5 6 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MyAnnotation { String value () default "" ; int count () default 0 ; }
使用注解 :
1 2 3 4 @MyAnnotation(value = "test", count = 5) public void method () { }
读取注解 :
1 2 3 Method method = clazz.getMethod("method" );MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);String value = annotation.value();
一般问题
Q1: 反射的应用场景?
A:
应用场景 :
框架开发 :Spring的IoC、MyBatis的Mapper
动态代理 :JDK动态代理、CGLIB
序列化 :JSON序列化、XML解析
工具类 :BeanUtils、ReflectionUtils
优缺点 :
优点 :灵活性高,可以实现动态功能
缺点 :性能较低,代码可读性差,安全性问题
Q2: 注解的处理方式?
A:
处理方式 :
编译时处理 :APT(Annotation Processing Tool)
运行时处理 :通过反射读取注解
字节码处理 :在字节码层面处理注解
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { String value () ; } Class<?> clazz = MyClass.class; if (clazz.isAnnotationPresent(MyAnnotation.class)) { MyAnnotation annotation = clazz.getAnnotation(MyAnnotation.class); String value = annotation.value(); }
困难问题
Q1: 反射的性能问题?
A:
性能问题 :
反射调用比直接调用慢很多(约10-100倍)
原因:方法查找、参数检查、安全检查等
优化方法 :
缓存Class对象 :避免重复获取
缓存Method/Field :避免重复查找
使用MethodHandle :JDK7+,性能更好
避免频繁反射 :在初始化时反射,运行时直接调用
示例 :
1 2 3 4 5 6 7 8 9 private static final Method getNameMethod;static { try { getNameMethod = Person.class.getMethod("getName" ); } catch (Exception e) { throw new RuntimeException (e); } }
Q2: 动态代理的实现?
A:
JDK动态代理 :
基于接口,使用Proxy.newProxyInstance
需要实现InvocationHandler
只能代理接口
CGLIB动态代理 :
基于继承,使用Enhancer
需要实现MethodInterceptor
可以代理类
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 InvocationHandler handler = (proxy, method, args) -> { Object result = method.invoke(target, args); return result; }; Object proxy = Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler ); Enhancer enhancer = new Enhancer ();enhancer.setSuperclass(TargetClass.class); enhancer.setCallback(new MethodInterceptor () { @Override public Object intercept (Object obj, Method method, Object[] args, MethodProxy proxy) { return proxy.invokeSuper(obj, args); } }); TargetClass proxy = (TargetClass) enhancer.create();
1.1.8 IO流 基础问题
Q1: IO流的分类?
A:
按流向分类 :
输入流 :InputStream、Reader
输出流 :OutputStream、Writer
按数据类型分类 :
字节流 :InputStream、OutputStream
字符流 :Reader、Writer
常用流 :
FileInputStream/FileOutputStream:文件字节流
FileReader/FileWriter:文件字符流
BufferedInputStream/BufferedOutputStream:缓冲字节流
BufferedReader/BufferedWriter:缓冲字符流
ObjectInputStream/ObjectOutputStream:对象流
Q2: 字节流和字符流的区别?
A:
字节流 :
以字节为单位读写
适合二进制文件(图片、视频等)
InputStream/OutputStream
字符流 :
以字符为单位读写
适合文本文件
Reader/Writer
内部使用字节流+字符编码
选择原则 :
文本文件:使用字符流
二进制文件:使用字节流
需要缓冲:使用Buffered系列
Q3: try-with-resources?
A:
try-with-resources :
Java 7引入,自动关闭资源
资源必须实现AutoCloseable接口
比try-finally更简洁
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 FileInputStream fis = null ;try { fis = new FileInputStream ("file.txt" ); } finally { if (fis != null ) { fis.close(); } } try (FileInputStream fis = new FileInputStream ("file.txt" ); BufferedInputStream bis = new BufferedInputStream (fis)) { }
一般问题
Q1: NIO是什么?
A:
NIO(New IO) :
Java 4引入的非阻塞IO
核心组件:Channel、Buffer、Selector
支持非阻塞IO和选择器
NIO vs IO :
IO :面向流,阻塞IO
NIO :面向缓冲区,非阻塞IO,支持选择器
核心组件 :
Channel :通道,类似流但可以双向
Buffer :缓冲区,数据容器
Selector :选择器,多路复用
示例 :
1 2 3 4 FileChannel channel = FileChannel.open(Paths.get("file.txt" ));ByteBuffer buffer = ByteBuffer.allocate(1024 );int bytesRead = channel.read(buffer);
Q2: 序列化和反序列化?
A:
序列化 :
将对象转换为字节流
实现Serializable接口
使用ObjectOutputStream
反序列化 :
将字节流转换为对象
使用ObjectInputStream
注意事项 :
需要serialVersionUID
transient字段不序列化
static字段不序列化
父类需要可序列化
示例 :
1 2 3 4 5 6 7 8 9 ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("obj.dat" ));oos.writeObject(obj); oos.close(); ObjectInputStream ois = new ObjectInputStream (new FileInputStream ("obj.dat" ));Object obj = ois.readObject();ois.close();
困难问题
Q1: 如何实现大文件的高效读写?
A:
优化方法 :
使用缓冲流 :BufferedInputStream/BufferedOutputStream
分块读取 :不要一次性读取整个文件
使用NIO :FileChannel + MappedByteBuffer(内存映射)
多线程处理 :分块并行处理
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 try (BufferedInputStream bis = new BufferedInputStream ( new FileInputStream ("large.txt" ), 8192 ); BufferedOutputStream bos = new BufferedOutputStream ( new FileOutputStream ("output.txt" ), 8192 )) { byte [] buffer = new byte [8192 ]; int len; while ((len = bis.read(buffer)) != -1 ) { bos.write(buffer, 0 , len); } } FileChannel channel = FileChannel.open(Paths.get("large.txt" ));MappedByteBuffer buffer = channel.map( FileChannel.MapMode.READ_ONLY, 0 , channel.size());
1.1.9 设计模式 基础问题
Q1: 设计模式的分类?
A:
创建型模式 :单例、工厂、建造者、原型、抽象工厂
结构型模式 :适配器、装饰器、代理、外观、桥接、组合、享元
行为型模式 :策略、观察者、责任链、命令、状态、模板方法、迭代器、中介者、备忘录、访问者、解释器
Q2: 单例模式的实现方式?
A:
1. 饿汉式(线程安全) :
1 2 3 4 5 6 7 public class Singleton { private static final Singleton instance = new Singleton (); private Singleton () {} public static Singleton getInstance () { return instance; } }
2. 懒汉式(双重检查锁) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Singleton { private static volatile Singleton instance; private Singleton () {} public static Singleton getInstance () { if (instance == null ) { synchronized (Singleton.class) { if (instance == null ) { instance = new Singleton (); } } } return instance; } }
3. 静态内部类(推荐) :
1 2 3 4 5 6 7 8 9 public class Singleton { private Singleton () {} private static class Holder { private static final Singleton instance = new Singleton (); } public static Singleton getInstance () { return Holder.instance; } }
4. 枚举(推荐,防止反射和序列化破坏) :
1 2 3 4 public enum Singleton { INSTANCE; public void doSomething () {} }
Q3: 工厂模式的使用场景?
A:
简单工厂 :根据参数创建不同类型的对象
工厂方法 :定义一个创建对象的接口,让子类决定实例化哪个类
抽象工厂 :提供一个创建一系列相关或相互依赖对象的接口
示例(简单工厂) :
1 2 3 4 5 6 7 8 9 10 public class ProductFactory { public static Product createProduct (String type) { if ("A" .equals(type)) { return new ProductA (); } else if ("B" .equals(type)) { return new ProductB (); } return null ; } }
一般问题
Q1: 代理模式的实现方式?
A:
1. 静态代理 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 interface Subject { void request () ; } class RealSubject implements Subject { public void request () { System.out.println("真实请求" ); } } class Proxy implements Subject { private RealSubject realSubject; public void request () { if (realSubject == null ) { realSubject = new RealSubject (); } System.out.println("代理前处理" ); realSubject.request(); System.out.println("代理后处理" ); } }
2. JDK动态代理 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 interface Subject { void request () ; } class RealSubject implements Subject { public void request () { System.out.println("真实请求" ); } } class ProxyHandler implements InvocationHandler { private Object target; public ProxyHandler (Object target) { this .target = target; } public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { System.out.println("代理前处理" ); Object result = method.invoke(target, args); System.out.println("代理后处理" ); return result; } } Subject proxy = (Subject) Proxy.newProxyInstance( RealSubject.class.getClassLoader(), new Class []{Subject.class}, new ProxyHandler (new RealSubject ()) );
3. CGLIB动态代理 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class RealSubject { public void request () { System.out.println("真实请求" ); } } class ProxyInterceptor implements MethodInterceptor { public Object intercept (Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("代理前处理" ); Object result = proxy.invokeSuper(obj, args); System.out.println("代理后处理" ); return result; } } Enhancer enhancer = new Enhancer ();enhancer.setSuperclass(RealSubject.class); enhancer.setCallback(new ProxyInterceptor ()); RealSubject proxy = (RealSubject) enhancer.create();
Q2: 观察者模式的实现?
A:
定义 :定义对象间一对多的依赖关系,当一个对象状态改变时,所有依赖它的对象都会收到通知。
实现 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 interface Observer { void update (String message) ; } class Subject { private List<Observer> observers = new ArrayList <>(); private String state; public void attach (Observer observer) { observers.add(observer); } public void detach (Observer observer) { observers.remove(observer); } public void setState (String state) { this .state = state; notifyObservers(); } private void notifyObservers () { for (Observer observer : observers) { observer.update(state); } } } class ConcreteObserver implements Observer { private String name; public ConcreteObserver (String name) { this .name = name; } public void update (String message) { System.out.println(name + "收到消息: " + message); } }
应用场景 :
Java中的事件监听机制
Spring的事件发布机制
MVC架构中的模型-视图关系
Q3: 策略模式的使用?
A:
定义 :定义一系列算法,把它们封装起来,并且使它们可以互相替换。
实现 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 interface Strategy { int execute (int a, int b) ; } class AddStrategy implements Strategy { public int execute (int a, int b) { return a + b; } } class MultiplyStrategy implements Strategy { public int execute (int a, int b) { return a * b; } } class Context { private Strategy strategy; public Context (Strategy strategy) { this .strategy = strategy; } public int executeStrategy (int a, int b) { return strategy.execute(a, b); } } Context context = new Context (new AddStrategy ());int result = context.executeStrategy(5 , 3 );
优势 :
算法可以自由切换
避免多重if-else判断
扩展性好,易于添加新策略
Q4: 适配器模式的应用?
A:
定义 :将一个类的接口转换成客户希望的另一个接口,使原本不兼容的类可以一起工作。
类适配器 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 interface Target { void request () ; } class Adaptee { public void specificRequest () { System.out.println("特殊请求" ); } } class Adapter extends Adaptee implements Target { public void request () { specificRequest(); } }
对象适配器 :
1 2 3 4 5 6 7 8 9 class Adapter implements Target { private Adaptee adaptee; public Adapter (Adaptee adaptee) { this .adaptee = adaptee; } public void request () { adaptee.specificRequest(); } }
应用场景 :
Java中的InputStreamReader(字节流适配字符流)
Spring MVC中的HandlerAdapter
第三方库接口适配
困难问题
Q1: 责任链模式的实现和应用?
A:
定义 :将请求沿着处理者链传递,直到有处理者处理它。
实现 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 abstract class Handler { protected Handler next; public void setNext (Handler next) { this .next = next; } public abstract void handleRequest (Request request) ; } class ConcreteHandler1 extends Handler { public void handleRequest (Request request) { if (request.getType() == RequestType.TYPE1) { System.out.println("Handler1处理请求" ); } else if (next != null ) { next.handleRequest(request); } } } class ConcreteHandler2 extends Handler { public void handleRequest (Request request) { if (request.getType() == RequestType.TYPE2) { System.out.println("Handler2处理请求" ); } else if (next != null ) { next.handleRequest(request); } } }
应用场景 :
Java中的异常处理机制
Servlet中的Filter链
Spring Security的过滤器链
审批流程
Q2: 装饰器模式与代理模式的区别?
A:
装饰器模式 :
目的 :动态地给对象添加额外的职责
关注点 :增强功能
关系 :装饰器和被装饰对象实现同一接口
示例 :Java IO流(BufferedReader装饰InputStreamReader)
代理模式 :
目的 :控制对对象的访问
关注点 :访问控制
关系 :代理和被代理对象实现同一接口
示例 :Spring AOP、RPC框架
代码对比 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Decorator implements Component { private Component component; public Decorator (Component component) { this .component = component; } public void operation () { component.operation(); } } class Proxy implements Subject { private RealSubject realSubject; public void request () { if (checkAccess()) { realSubject.request(); } } }
Q3: 模板方法模式的应用?
A:
定义 :定义一个操作中算法的骨架,而将一些步骤延迟到子类中。
实现 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 abstract class AbstractClass { public final void templateMethod () { step1(); step2(); step3(); } protected void step1 () { System.out.println("步骤1" ); } protected abstract void step2 () ; protected void step3 () { System.out.println("步骤3" ); } } class ConcreteClass extends AbstractClass { protected void step2 () { System.out.println("具体步骤2" ); } }
应用场景 :
Spring中的JdbcTemplate
框架中的钩子方法
算法骨架固定,部分步骤可变
Q4: 建造者模式的使用场景?
A:
定义 :将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
实现 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 class Product { private String part1; private String part2; private String part3; private Product (Builder builder) { this .part1 = builder.part1; this .part2 = builder.part2; this .part3 = builder.part3; } public static class Builder { private String part1; private String part2; private String part3; public Builder part1 (String part1) { this .part1 = part1; return this ; } public Builder part2 (String part2) { this .part2 = part2; return this ; } public Builder part3 (String part3) { this .part3 = part3; return this ; } public Product build () { return new Product (this ); } } } Product product = new Product .Builder() .part1("部件1" ) .part2("部件2" ) .part3("部件3" ) .build();
应用场景 :
创建复杂对象,参数多且可选
需要保证对象创建过程的完整性
链式调用,代码可读性好
示例:StringBuilder、OkHttp的Request.Builder
Q5: 设计模式在实际项目中的应用?
A:
Spring框架中的应用 :
单例模式 :Bean默认单例
工厂模式 :BeanFactory、ApplicationContext
代理模式 :AOP动态代理
模板方法 :JdbcTemplate、RestTemplate
观察者模式 :事件发布机制
适配器模式 :HandlerAdapter
JDK中的应用 :
迭代器模式 :Iterator接口
适配器模式 :InputStreamReader
装饰器模式 :IO流装饰器
观察者模式 :Observer接口(已废弃,但思想保留)
单例模式 :Runtime类
实际开发建议 :
不要过度设计,优先考虑简单方案
理解模式本质,而非死记硬背
结合业务场景选择合适的模式
注意模式之间的组合使用
1.2 Spring框架 基础问题
Q1: Spring IoC和AOP是什么?
A:
IoC(控制反转):对象的创建和依赖关系由Spring容器管理
AOP(面向切面编程):在不修改原有代码的情况下增强功能
IoC通过依赖注入实现,AOP通过动态代理实现
Q2: Spring Bean的作用域?
A:
singleton:单例(默认)
prototype:每次创建新实例
request:每个HTTP请求一个实例
session:每个HTTP会话一个实例
globalSession:全局HTTP会话
Q3: @Autowired和@Resource的区别?
A:
@Autowired:Spring注解,按类型注入,可配合@Qualifier按名称
@Resource:JSR-250注解,按名称注入,找不到再按类型
@Autowired支持required=false,@Resource不支持
一般问题
Q1: Spring IoC和AOP的原理?
A:
IoC(控制反转):通过反射和工厂模式创建对象,管理对象生命周期和依赖关系
AOP(面向切面编程):基于动态代理(JDK动态代理或CGLIB)实现横切关注点
JDK动态代理:基于接口,使用InvocationHandler
CGLIB代理:基于继承,通过ASM字节码技术生成子类
Q2: Spring Boot自动配置原理?
A:
通过@EnableAutoConfiguration注解启用
扫描META-INF/spring.factories文件中的自动配置类
使用@ConditionalOnXxx条件注解判断是否生效
通过starter机制简化依赖管理
Q3: Spring事务传播机制?
A:
REQUIRED(默认):存在事务则加入,不存在则创建
REQUIRES_NEW:总是创建新事务
SUPPORTS:存在则加入,不存在则以非事务方式执行
NOT_SUPPORTED:以非事务方式执行
MANDATORY:必须在事务中执行,否则抛异常
NEVER:不能在事务中执行
NESTED:嵌套事务
困难问题
Q1: Spring如何解决循环依赖?
A:
三级缓存 :
singletonObjects:完整Bean
earlySingletonObjects:早期Bean(未完成属性注入)
singletonFactories:Bean工厂
解决过程 :A依赖B,B依赖A时,先创建A的早期对象放入缓存,再创建B,B注入A的早期对象,最后完成A的属性注入
Q2: Spring AOP的实现原理?
A:
JDK动态代理 :基于接口,使用Proxy.newProxyInstance创建代理对象
CGLIB代理 :基于继承,生成目标类的子类作为代理类
选择规则 :有接口用JDK,无接口用CGLIB
织入时机 :编译期、类加载期、运行期(Spring使用运行期)
Q3: Spring事务失效的场景?
A:
方法非public
方法被final或static修饰
同一类内部方法调用(未通过代理)
异常被捕获未抛出
数据库不支持事务
事务传播行为设置不当
1.3 MyBatis 基础问题
Q1: MyBatis的缓存机制?
A:
一级缓存:SqlSession级别,默认开启,同一SqlSession中相同查询会缓存
二级缓存:Mapper级别,需要手动开启,跨SqlSession共享
缓存更新策略:insert/update/delete会清空相关缓存
Q2: MyBatis如何防止SQL注入?
A:
使用#{}占位符,MyBatis会进行预编译,参数作为字符串处理
避免使用${},会直接拼接SQL,存在注入风险
对用户输入进行校验和转义
一般问题
Q1: MyBatis的执行流程?
A:
加载配置文件,创建SqlSessionFactory
创建SqlSession
通过SqlSession获取Mapper代理对象
执行SQL(解析SQL、设置参数、执行、结果映射)
提交事务,关闭SqlSession
Q2: #{}和${}的区别?
A:
#{}:预编译,参数作为字符串处理,防止SQL注入
${}:字符串替换,直接拼接SQL,存在SQL注入风险
#{}适用于参数值,${}适用于表名、列名等动态部分
困难问题
Q1: MyBatis的插件机制?
A:
基于拦截器(Interceptor)实现
可以拦截Executor、StatementHandler、ParameterHandler、ResultSetHandler
实现Interceptor接口,使用@Intercepts注解指定拦截的方法
常见应用:分页插件、性能监控插件
Q2: MyBatis如何实现延迟加载?
A:
通过动态代理实现
配置lazyLoadingEnabled=true开启延迟加载
关联对象只有在真正访问时才会加载
可以设置aggressiveLazyLoading控制加载行为
二、大数据技术栈 2.1 Flink 基础问题
Q1: Flink的核心概念?
A:
流批一体 :同一套API支持流处理和批处理
时间语义 :
Event Time:事件产生的时间
Processing Time:处理时间
Ingestion Time:数据进入Flink的时间
状态(State) :KeyedState和OperatorState,支持状态后端(Memory、RocksDB)
检查点(Checkpoint) :基于Chandy-Lamport算法实现分布式快照
水印(Watermark) :用于处理乱序数据,表示事件时间的进度
Q2: Flink与Spark Streaming的区别?
A:
Flink是真正的流处理,延迟更低(毫秒级)
Spark Streaming是微批处理,延迟较高(秒级)
Flink支持事件时间,Spark Streaming主要支持处理时间
Flink的状态管理更完善
Q3: Flink的并行度如何设置?
A:
可以在代码中设置:env.setParallelism(4)
可以在算子级别设置:dataStream.map(...).setParallelism(2)
可以在配置文件中设置全局并行度
并行度应该根据数据量和资源情况设置
一般问题
Q1: Flink如何保证Exactly-Once语义?
A:
Checkpoint机制 :定期创建全局一致性快照
两阶段提交 :配合支持事务的外部系统(如Kafka)
状态后端 :RocksDB支持大状态持久化
端到端一致性 :Source和Sink都支持事务
Q2: Flink如何处理数据倾斜?
A:
KeyBy前加随机前缀 :打散热点key
使用LocalKeyBy :在数据量大的key上先做本地聚合
调整并行度 :增加下游算子并行度
使用Rebalance :强制数据重新分布
自定义分区器 :根据业务特点自定义分区策略
Q3: Flink与Kafka的集成?
A:
FlinkKafkaConsumer:支持从Kafka消费数据
FlinkKafkaProducer:支持写入Kafka
支持Kafka事务,保证Exactly-Once
支持从指定offset开始消费
支持动态发现新分区
困难问题
Q1: Flink的Checkpoint机制原理?
A:
基于Chandy-Lamport分布式快照算法
JobManager触发Checkpoint,向所有Source发送barrier
Source收到barrier后做快照,然后向下游发送barrier
算子收到所有输入的barrier后做快照
所有算子完成快照后,Checkpoint完成
失败时从最近的Checkpoint恢复
Q2: Flink的背压(Backpressure)机制?
A:
当下游处理速度慢于上游时,上游会减慢发送速度
通过TCP流控机制实现
可以通过监控反压指标定位性能瓶颈
解决方案:增加并行度、优化算子逻辑、调整缓冲区大小
Q3: Flink的状态后端选择?
A:
MemoryStateBackend :状态存储在内存,适合小状态、测试
FsStateBackend :状态存储在文件系统,适合中等状态
RocksDBStateBackend :状态存储在RocksDB,适合大状态、生产环境
选择原则:根据状态大小和性能要求选择
2.2 Spark 基础问题
Q1: Spark的核心概念(RDD、DAG、Stage)?
A:
RDD(弹性分布式数据集) :不可变的分布式数据集合,支持容错
DAG(有向无环图) :表示RDD之间的依赖关系
Stage(阶段) :根据shuffle依赖划分,分为ShuffleMapStage和ResultStage
Task :Stage中的最小执行单元
Q2: RDD的五大特性?
A:
分区列表 :RDD由多个分区组成
计算函数 :每个分区都有计算函数
依赖关系 :RDD之间存在依赖关系
分区器 :可选,用于shuffle
优先位置 :可选,用于数据本地性
Q3: Spark的宽依赖和窄依赖?
A:
窄依赖 :父RDD的每个分区最多被一个子RDD分区使用(map、filter)
宽依赖 :父RDD的每个分区被多个子RDD分区使用(groupBy、join)
宽依赖会触发shuffle,是Stage划分的依据
一般问题
Q1: Spark的Shuffle过程?
A:
Map端 :
数据写入环形缓冲区(默认100MB)
达到阈值后spill到磁盘,按key排序
合并多个spill文件
生成索引文件,记录每个分区的数据位置
Reduce端 :
通过网络拉取Map端的数据
合并排序后交给reduce处理
Q2: Spark Shuffle优化?
A:
调整shuffle分区数 :spark.sql.shuffle.partitions
使用Kryo序列化 :减少序列化开销
启用map端聚合 :spark.sql.mapSideJoin
调整缓冲区大小 :spark.shuffle.file.buffer
使用Sort Shuffle :默认算法,性能更好
Q3: Spark SQL优化?
A:
使用列式存储 :Parquet、ORC格式
分区裁剪 :只读取需要的分区
谓词下推 :在数据源层面过滤数据
列裁剪 :只读取需要的列
广播Join :小表广播到所有节点
调整并行度 :根据数据量设置合理的分区数
困难问题
Q1: Spark的内存管理?
A:
堆内存划分 :
Storage Memory:缓存RDD和广播变量
Execution Memory:shuffle、join等操作
User Memory:用户代码和数据结构
Reserved Memory:系统保留
动态占用 :Execution和Storage可以互相借用
溢出机制 :内存不足时spill到磁盘
Q2: 如何通过Spark UI定位性能瓶颈?
A:
查看Stage执行时间 :找出耗时最长的Stage
查看Task分布 :检查是否有数据倾斜(某些Task执行时间过长)
查看Shuffle读写 :检查Shuffle数据量是否过大
查看GC时间 :检查是否有GC问题
查看数据倾斜 :通过Task执行时间分布判断
Q3: Spark的容错机制?
A:
RDD容错 :通过Lineage(血缘)重建丢失的分区
Checkpoint :将RDD持久化到可靠存储,避免长血缘链
Shuffle容错 :通过MapOutputTracker记录shuffle输出位置
Executor容错 :失败时重新调度Task到其他Executor
2.3 Kafka 基础问题
Q1: Kafka的架构和核心概念?
A:
Producer :消息生产者
Broker :Kafka服务器节点
Topic :消息主题,逻辑概念
Partition :分区,物理存储单元
Consumer :消息消费者
Consumer Group :消费者组,实现负载均衡
Replica :副本,保证高可用
Leader/Follower :主副本和从副本
Q2: Kafka为什么这么快?
A:
顺序写入 :磁盘顺序写入性能接近内存随机写入
零拷贝 :使用sendfile系统调用,减少数据拷贝
批量发送 :批量发送消息,减少网络开销
分区并行 :多个分区并行处理
页缓存 :利用操作系统页缓存
Q3: Kafka的消费方式(Pull vs Push)?
A:
Kafka采用Pull模式 :Consumer主动拉取消息
优点:
Consumer可以控制消费速率
可以批量消费,提高吞吐量
简化Broker设计
Push模式的问题:
难以适应不同消费速率的Consumer
可能导致Consumer过载
一般问题
Q1: Kafka如何保证消息不丢失?
A:
Producer端 :
设置acks=all(或-1),等待所有ISR副本确认
设置retries重试机制
使用同步发送或回调确认
Broker端 :
设置replication.factor>=2,多副本
设置min.insync.replicas>=2,保证ISR中有足够副本
设置unclean.leader.election.enable=false,禁止非ISR副本成为Leader
Consumer端 :
关闭自动提交:enable.auto.commit=false
处理完消息后再手动提交offset
Q2: Kafka如何保证消息顺序性?
A:
单分区内有序 :Kafka保证单个分区内消息有序
Producer端 :使用相同的key,消息会发送到同一分区
Consumer端 :单线程消费或使用单线程处理同一key的消息
注意 :如果开启重试,可能破坏顺序性,需要设置max.in.flight.requests.per.connection=1
Q3: Kafka的副本机制?
A:
每个Partition有多个副本(replica)
一个副本作为Leader,负责读写
其他副本作为Follower,从Leader同步数据
ISR(In-Sync Replicas):与Leader同步的副本集合
Leader选举:当Leader失效时,从ISR中选择新Leader
困难问题
Q1: Kafka的Consumer Rebalance?
A:
触发条件:Consumer加入/退出、Partition数量变化
过程:
所有Consumer停止消费
重新分配Partition
恢复消费
问题:Rebalance期间无法消费,影响可用性
优化:使用增量Rebalance(StickyAssignor)
Q2: Kafka的幂等性?
A:
Producer端幂等:设置enable.idempotence=true
通过Producer ID(PID)和序列号(Sequence Number)实现
保证单会话、单分区内的幂等性
配合事务可以实现跨分区、跨会话的幂等性
Q3: Kafka的事务机制?
A:
支持跨分区、跨会话的事务
使用事务协调器(TransactionCoordinator)管理事务
Producer发送事务消息,提交或回滚事务
Consumer可以读取已提交的事务消息
配合幂等性保证Exactly-Once语义
Q4: Kafka vs RocketMQ的对比?
A:
架构对比 :
特性
Kafka
RocketMQ
架构模式
分布式流式平台
分布式消息中间件
存储模型
基于日志文件(Log Segment)
基于CommitLog + ConsumeQueue
消息模型
发布订阅、点对点(通过Consumer Group)
发布订阅、点对点、顺序消息、事务消息
消费模式
Pull模式
Pull模式(支持Push模式)
消息顺序
单分区有序
单队列有序,支持全局顺序
消息过滤
基于Consumer Group
支持Tag过滤、SQL过滤
延迟消息
不支持(需要外部实现)
支持18个延迟级别
消息重试
需要Consumer自己实现
支持自动重试,可配置重试次数和间隔
死信队列
需要自己实现
支持死信队列
事务消息
支持(Kafka 0.11+)
支持(两阶段提交)
消息追踪
需要外部工具
内置消息轨迹
多语言支持
支持多种语言
主要支持Java,其他语言支持较少
性能对比 :
特性
Kafka
RocketMQ
吞吐量
极高(百万级TPS)
高(十万级TPS)
延迟
毫秒级
毫秒级(Push模式更低)
消息堆积
支持海量消息堆积
支持大量消息堆积
顺序消息
单分区有序
单队列有序,性能更好
批量消息
支持
支持,性能优化更好
存储机制对比 :
Kafka :
基于日志文件(Log Segment)
顺序写入,性能高
使用零拷贝技术
消息按时间或大小切分Segment
RocketMQ :
CommitLog:所有消息顺序写入
ConsumeQueue:按Topic和Queue索引,提高查询性能
支持消息刷盘策略(同步/异步)
支持消息压缩
使用场景对比 :
Kafka适合 :
大数据流处理 :日志收集、实时数据管道
事件溯源 :事件驱动架构
流式计算 :配合Flink、Spark Streaming
高吞吐场景 :需要极高吞吐量
多语言生态 :需要多语言客户端支持
RocketMQ适合 :
业务消息 :订单、支付等业务消息
事务消息 :需要强一致性的事务场景
顺序消息 :需要保证消息顺序
延迟消息 :需要延迟投递
消息过滤 :需要复杂的消息过滤
Java生态 :主要使用Java技术栈
运维对比 :
特性
Kafka
RocketMQ
运维复杂度
较高(需要ZooKeeper)
较低(NameServer轻量级)
监控工具
Kafka Manager、Confluent Control Center
RocketMQ Console、Prometheus
社区支持
Apache顶级项目,社区活跃
阿里开源,国内社区活跃
文档
英文文档为主
中文文档完善
学习曲线
较陡
相对平缓
选择建议 :
选择Kafka :
需要极高的吞吐量
大数据流处理场景
需要多语言客户端支持
事件驱动架构
配合流式计算框架(Flink、Spark)
选择RocketMQ :
业务消息场景
需要事务消息
需要延迟消息
需要消息过滤
主要使用Java技术栈
需要更好的中文支持和文档
总结 :
Kafka :更适合大数据、流处理场景,追求极致性能
RocketMQ :更适合业务消息场景,功能更丰富,更适合Java生态
Q5: Kafka vs RabbitMQ的对比?
A:
架构对比 :
特性
Kafka
RabbitMQ
架构模式
分布式流式平台
消息代理(Message Broker)
存储模型
基于日志文件(持久化到磁盘)
内存 + 磁盘(可配置)
消息模型
发布订阅、点对点(通过Consumer Group)
发布订阅、点对点、路由(Direct、Topic、Fanout、Headers)
消费模式
Pull模式
Push模式(AMQP协议)
消息顺序
单分区有序
单队列有序
消息确认
Offset机制
ACK机制(自动/手动)
消息持久化
默认持久化
可配置(durable)
消息路由
基于Partition
支持Exchange和Routing Key
消息过滤
基于Consumer Group
支持Exchange类型和Binding
延迟消息
不支持(需要外部实现)
支持延迟队列插件
消息优先级
不支持
支持(Priority Queue)
死信队列
需要自己实现
支持(Dead Letter Exchange)
事务消息
支持
支持(事务模式)
多语言支持
支持多种语言
支持多种语言(AMQP标准)
性能对比 :
特性
Kafka
RabbitMQ
吞吐量
极高(百万级TPS)
中等(万级TPS)
延迟
毫秒级
微秒级(内存模式)
消息堆积
支持海量消息堆积
受内存限制,不适合大量堆积
顺序消息
单分区有序,性能好
单队列有序,性能一般
批量消息
支持,性能优化好
支持,但性能不如Kafka
存储机制对比 :
Kafka :
基于日志文件(Log Segment)
顺序写入磁盘,性能高
使用零拷贝技术
消息持久化到磁盘,支持海量存储
消息按时间或大小切分Segment
RabbitMQ :
内存 + 磁盘混合存储
消息可以存储在内存或磁盘
支持消息持久化(durable)
内存模式性能高,但受内存限制
磁盘模式性能较低,但更可靠
消息路由机制对比 :
Kafka :
基于Partition路由
Producer指定key,相同key路由到同一Partition
简单直接,适合流式处理
RabbitMQ :
基于Exchange和Routing Key
Exchange类型:
Direct :精确匹配Routing Key
Topic :模式匹配Routing Key
Fanout :广播到所有队列
Headers :基于消息头匹配
灵活的路由机制,适合复杂业务场景
可靠性对比 :
特性
Kafka
RabbitMQ
消息持久化
默认持久化
可配置
消息确认
Offset机制
ACK机制(自动/手动)
高可用
副本机制(Replication)
镜像队列(Mirrored Queue)
消息丢失
配置正确时不会丢失
配置正确时不会丢失
消息重复
可能重复(需要幂等处理)
可能重复(需要幂等处理)
使用场景对比 :
Kafka适合 :
大数据流处理 :日志收集、实时数据管道
事件溯源 :事件驱动架构
流式计算 :配合Flink、Spark Streaming
高吞吐场景 :需要极高吞吐量
消息堆积 :需要支持海量消息堆积
数据管道 :作为数据管道连接不同系统
RabbitMQ适合 :
业务消息 :订单、支付等业务消息
复杂路由 :需要灵活的消息路由
低延迟 :需要微秒级延迟(内存模式)
任务队列 :异步任务处理
RPC调用 :请求/响应模式
消息确认 :需要精确的消息确认机制
运维对比 :
特性
Kafka
RabbitMQ
运维复杂度
较高(需要ZooKeeper)
中等(Erlang运行时)
监控工具
Kafka Manager、Confluent Control Center
RabbitMQ Management UI、Prometheus
社区支持
Apache顶级项目,社区活跃
Pivotal支持,社区活跃
文档
英文文档为主
英文文档完善
学习曲线
较陡
中等
集群管理
需要ZooKeeper协调
支持集群模式
协议支持对比 :
Kafka :
RabbitMQ :
AMQP :标准消息队列协议
MQTT :物联网协议(通过插件)
STOMP :简单文本协议(通过插件)
HTTP :REST API
协议丰富,兼容性好
选择建议 :
选择Kafka :
需要极高的吞吐量(百万级TPS)
大数据流处理场景
需要支持海量消息堆积
事件驱动架构
配合流式计算框架(Flink、Spark)
作为数据管道
选择RabbitMQ :
业务消息场景
需要灵活的消息路由
需要低延迟(微秒级)
需要复杂的消息确认机制
需要标准协议支持(AMQP)
任务队列、异步处理
需要请求/响应模式(RPC)
总结 :
Kafka :更适合大数据、流处理场景,追求极致吞吐量和消息堆积能力
RabbitMQ :更适合业务消息场景,提供灵活的路由和丰富的协议支持,适合复杂的业务逻辑
三种消息中间件对比总结 :
特性
Kafka
RocketMQ
RabbitMQ
吞吐量
极高(百万级)
高(十万级)
中等(万级)
延迟
毫秒级
毫秒级
微秒级(内存)
消息堆积
海量
大量
受内存限制
路由机制
简单(Partition)
简单(Queue)
灵活(Exchange)
协议支持
自定义
自定义
AMQP/MQTT/STOMP
适用场景
大数据流处理
业务消息(Java)
业务消息(通用)
运维复杂度
高
中
中
社区
Apache
阿里
Pivotal
2.4 Hadoop生态 2.4.1 HDFS 基础问题
Q1: HDFS的架构?
A:
NameNode :管理文件系统命名空间,存储元数据
DataNode :存储实际数据块
Secondary NameNode :辅助NameNode,定期合并fsimage和edits
Block :默认128MB,是数据存储和复制的单位
Q2: HDFS的读写流程?
A:
写流程 :
Client向NameNode请求写入文件
NameNode返回DataNode列表
Client将数据写入第一个DataNode
DataNode之间流水线复制数据
所有副本写入成功后返回确认
读流程 :
Client向NameNode请求读取文件
NameNode返回DataNode列表和Block位置
Client从最近的DataNode读取数据
一般问题
Q1: HDFS的高可用(HA)?
A:
使用ZooKeeper实现NameNode的HA
Active NameNode和Standby NameNode
通过JournalNode同步元数据变更
自动故障转移,保证服务可用性
Q2: HDFS的Block大小为什么是128MB?
A:
减少NameNode的元数据量
减少网络传输开销
平衡寻址时间和传输时间
适合MapReduce等大数据处理框架
困难问题
Q1: HDFS的元数据管理?
A:
fsimage :文件系统镜像,存储完整的命名空间
edits :编辑日志,记录文件系统的变更
Secondary NameNode定期合并fsimage和edits
NameNode启动时加载fsimage并重放edits
Q2: HDFS的副本放置策略?
A:
第一个副本:放在Client所在的节点
第二个副本:放在不同机架的节点
第三个副本:放在第二个副本相同机架的不同节点
目的:提高可靠性和读取性能
2.4.2 YARN 基础问题
Q1: YARN的架构和工作流程?
A:
ResourceManager :资源管理器,包含Scheduler和ApplicationsManager
NodeManager :节点管理器,管理单个节点的资源
ApplicationMaster :应用主控程序,管理应用生命周期
Container :资源抽象,封装CPU、内存等资源
工作流程 :
Client提交应用
RM分配Container启动AM
AM向RM申请资源
RM分配Container给AM
AM与NM通信启动Task
Task运行并汇报状态
应用完成后AM注销
一般问题
Q1: YARN的调度算法?
A:
FIFO :先进先出,简单但不适合多用户
Capacity Scheduler :容量调度器,按队列分配资源
Fair Scheduler :公平调度器,公平分配资源
DRF(Dominant Resource Fairness) :主资源公平调度
困难问题
Q1: YARN的资源分配机制?
A:
使用Container抽象资源(CPU、内存)
Scheduler根据策略分配Container
支持资源抢占(Preemption)
支持资源预留(Reservation)
2.4.3 Hive 基础问题
Q1: Hive的执行流程?
A:
Parser :将HQL转换为AST(抽象语法树)
Semantic Analyzer :语义分析,转换为查询块
Logic Plan Generator :生成逻辑执行计划
Logic Optimizer :逻辑优化(谓词下推、分区裁剪等)
Physical Plan Generator :生成物理执行计划(MapReduce Jobs)
Physical Optimizer :物理优化(选择Join策略等)
Q2: Hive的数据倾斜问题?
A:
原因 :key分布不均匀、业务数据特性、建表考虑不周
解决方案 :
参数调节:hive.map.aggr=true、hive.groupby.skewindata=true
MapJoin:小表join大表使用MapJoin
空值处理:给空值赋予随机key
不同数据类型关联:统一数据类型
特殊情况单独处理:倾斜数据单独处理再union
一般问题
Q1: Hive的优化手段?
A:
合理使用分区和分桶
使用列式存储 :ORC、Parquet格式
Map端聚合 :hive.map.aggr=true
小文件合并 :减少小文件数量
合理设置Map和Reduce数量
使用压缩 :减少I/O开销
Join优化 :小表放左边,使用MapJoin
Q2: Hive的窗口函数?
A:
聚合型 :SUM、AVG、COUNT等配合OVER使用
分析型 :ROW_NUMBER、RANK、DENSE_RANK
取值型 :LAG、LEAD、FIRST_VALUE、LAST_VALUE
窗口大小 :ROWS BETWEEN … AND …
困难问题
Q1: Hive的Join优化策略?
A:
MapJoin :小表加载到内存,避免shuffle
Bucket Join :两个表都分桶,相同bucket的join
SMB Join :Sort Merge Bucket Join,两个表都分桶且排序
Skew Join :处理数据倾斜的join
Q2: Hive的元数据管理?
A:
元数据存储在关系型数据库(MySQL、PostgreSQL等)
包括表结构、分区信息、存储位置等
Metastore服务管理元数据访问
支持多版本Metastore
三、OLAP与数据存储 3.0 LSM Tree存储引擎基础 基础问题
Q1: LSM Tree的基本原理?
A:
LSM Tree定义 :
Log-Structured Merge Tree,日志结构合并树
由O’Neil等人在1996年提出
专为写密集型场景设计的高性能存储结构
核心思想 :
写入优化 :数据先写入内存(MemTable),顺序写入磁盘
批量合并 :后台线程定期合并多个数据文件
分层存储 :数据按时间顺序分层存储,新数据在高层,老数据在低层
基本结构 :
MemTable :内存中的有序数据结构(如跳表、B+树)
Immutable MemTable :MemTable写满后变为只读,等待刷盘
SSTable(Sorted String Table) :磁盘上的有序数据文件
多个Level :SSTable按大小和时间分层存储
Q2: LSM Tree的写入流程?
A:
写入过程 :
数据写入MemTable(内存,有序结构)
写入WAL(Write-Ahead Log)保证持久化
MemTable写满后,转换为Immutable MemTable
后台线程将Immutable MemTable刷盘为SSTable(Level 0)
后台线程定期合并Level i的SSTable到Level i+1
写入优势 :
顺序写入 :MemTable刷盘是顺序写入,性能高
无随机写 :避免B+树的随机写放大问题
高吞吐 :适合写密集型场景
Q3: LSM Tree的读取流程?
A:
读取过程 :
先查询MemTable(最新数据)
如果未找到,查询Immutable MemTable
如果仍未找到,从Level 0开始逐层查询SSTable
使用Bloom Filter快速过滤不存在的key
合并多个SSTable的查询结果
读取特点 :
可能需要查询多个SSTable :读放大问题
使用Bloom Filter :快速判断key是否存在
范围查询 :需要合并多个SSTable的结果
一般问题
Q1: LSM Tree的Compaction机制?
A:
Compaction目的 :
合并多个SSTable,减少文件数量
删除过期和重复的数据
优化数据布局,提高查询性能
Compaction策略 :
1. Size-Tiered Compaction(STCS) :
相同大小的SSTable合并
合并后大小翻倍
适合写密集型场景
问题:空间放大,需要更多存储空间
2. Leveled Compaction(LCS) :
每层SSTable大小相近
Level i的SSTable合并到Level i+1
每层大小限制:Level i+1是Level i的10倍
优势:空间放大小,查询性能好
问题:写放大较大
3. Time-Windowed Compaction(TWCS) :
按时间窗口合并
适合时序数据
可以设置TTL自动删除过期数据
Compaction触发条件 :
SSTable数量达到阈值
数据量达到阈值
手动触发
Q2: LSM Tree的优缺点?
A:
优点 :
写入性能高 :顺序写入,无随机写
高吞吐 :适合写密集型场景
压缩率高 :SSTable可以压缩存储
支持范围查询 :数据有序存储
缺点 :
读放大 :可能需要查询多个SSTable
写放大 :Compaction会重写数据
空间放大 :同一数据可能存在于多个SSTable
删除延迟 :删除操作需要Compaction才能真正删除
Q3: LSM Tree的优化技术?
A:
1. Bloom Filter :
快速判断key是否存在
减少不必要的SSTable查询
内存占用小,误判率低
2. 索引优化 :
SSTable内使用稀疏索引
快速定位数据在SSTable中的位置
3. 缓存策略 :
Block Cache:缓存SSTable的数据块
Table Cache:缓存SSTable的元数据
4. Compaction优化 :
选择合适的Compaction策略
控制Compaction频率
使用多线程并行Compaction
困难问题
Q1: LSM Tree的写放大和读放大?
A:
写放大(Write Amplification) :
定义 :实际写入磁盘的数据量 / 用户写入的数据量
原因 :
Compaction会重写数据
数据可能被写入多个Level
Leveled Compaction :写放大较大(约10-50倍)
Size-Tiered Compaction :写放大较小(约2-5倍)
优化 :
选择合适的Compaction策略
增大SSTable大小
减少Compaction频率
读放大(Read Amplification) :
定义 :实际读取的数据量 / 用户需要的数据量
原因 :
需要查询多个SSTable
需要读取整个SSTable的块
优化 :
使用Bloom Filter快速过滤
优化索引结构
使用缓存减少I/O
Q2: LSM Tree的Compaction策略选择?
A:
选择原则 :
写密集型 :选择Size-Tiered Compaction,写放大小
读密集型 :选择Leveled Compaction,读放大小
时序数据 :选择Time-Windowed Compaction
混合场景 :根据实际负载调整
Leveled Compaction特点 :
每层大小固定,数据分布均匀
查询时最多查询一层
空间放大小(约1.1倍)
写放大大(约10-50倍)
Size-Tiered Compaction特点 :
相同大小的SSTable合并
空间放大大(约2倍)
写放大小(约2-5倍)
适合写密集型场景
Q3: LSM Tree vs B+ Tree的对比?
A:
B+ Tree特点 :
写入 :随机写入,需要维护树结构
读取 :O(log n)时间复杂度,查询路径固定
适用场景 :读多写少,需要事务支持
LSM Tree特点 :
写入 :顺序写入,高吞吐
读取 :可能需要查询多个SSTable
适用场景 :写多读少,高吞吐写入
对比 :
特性
B+ Tree
LSM Tree
写入性能
随机写,性能低
顺序写,性能高
读取性能
O(log n),稳定
可能读放大
写放大
小(约1-2倍)
大(约2-50倍)
读放大
小(约1倍)
大(约2-10倍)
空间放大
小
中等
适用场景
读多写少
写多读少
3.1 Doris/StarRocks 基础问题
Q1: Doris的核心特性?
A:
MPP架构 :大规模并行处理
列式存储 :高效压缩和查询
向量化执行 :SIMD指令加速
物化视图 :预计算加速查询
实时更新 :支持流式导入和更新
MySQL协议兼容 :易于使用
Q2: Doris的表模型?
A:
Aggregate模型 :预聚合,适合汇总类查询
Unique模型 :主键唯一,支持更新
Duplicate模型 :明细数据,适合分析查询
选择原则 :根据查询场景选择合适模型
Q3: Doris/StarRocks的列式存储结构?
A:
列式存储原理 :
数据按列存储,而非按行存储
同一列的数据在物理上连续存储
查询时只读取需要的列,减少I/O
存储文件结构 :
数据文件 :按列存储,每列一个文件
索引文件 :前缀索引、Bloom Filter、ZoneMap等
元数据文件 :表结构、分区信息、统计信息
列式存储优势 :
压缩率高 :同类型数据连续存储,压缩效果好
查询高效 :只读取需要的列,减少I/O
向量化执行 :SIMD指令并行处理列数据
适合分析 :OLAP场景下优势明显
一般问题
Q1: Doris的物化视图?
A:
预计算的聚合结果,加速查询
自动路由:查询自动匹配物化视图
支持多物化视图:一个表可以有多个物化视图
增量更新:只更新变更数据
Q2: Doris的索引优化?
A:
前缀索引 :基于前36字节构建
Bloom Filter :快速判断数据是否存在
ZoneMap :Min/Max索引,用于范围查询
倒排索引 :用于文本搜索
Q3: Doris/StarRocks的数据文件组织?
A:
Rowset(行集) :
数据导入时生成Rowset,包含多个Segment
Rowset是不可变的,写入后不能修改
多个Rowset可以合并(Compaction)
Segment结构 :
列数据文件 :每列一个文件,列式存储
索引文件 :
Short Key Index:前缀索引,基于前36字节
Bloom Filter:快速判断数据是否存在
ZoneMap:Min/Max索引,用于范围查询
Footer :元数据信息,包括索引位置、统计信息等
数据压缩 :
支持多种压缩算法:LZ4、ZSTD、ZLIB等
列式存储压缩率高
可以设置压缩级别平衡压缩率和性能
分区和分桶 :
分区(Partition) :按时间或其他维度分区,减少扫描范围
分桶(Bucket) :数据分桶存储,提高并行度
分区和分桶的组合使用,优化查询性能
困难问题
Q1: Doris的查询优化?
A:
谓词下推 :在存储层过滤数据
列裁剪 :只读取需要的列
分区裁剪 :只扫描相关分区
Join优化 :Broadcast Join、Shuffle Join、Colocate Join
物化视图路由 :自动选择最优物化视图
Q2: Doris的导入性能优化?
A:
使用Stream Load批量导入
调整batch size和并发度
使用列式存储格式(Parquet、ORC)
合理设置分区和分桶
避免小文件问题
Q3: Doris/StarRocks的Compaction机制?
A:
Compaction目的 :
合并多个Rowset,减少文件数量
删除标记为删除的数据
优化数据布局,提高查询性能
Compaction策略 :
Base Compaction :合并Base数据和增量数据
Cumulative Compaction :合并增量数据
Full Compaction :全量合并,优化数据分布
Compaction触发条件 :
Rowset数量达到阈值
数据量达到阈值
手动触发
Compaction优化 :
选择合适的Compaction策略
控制Compaction频率,避免影响查询
监控Compaction进度和资源使用
3.2 ClickHouse 基础问题
Q1: ClickHouse的核心特性?
A:
列式存储 :高效压缩,只读取需要的列
向量化执行 :SIMD指令并行处理
MPP架构 :分布式并行查询
数据压缩 :多种压缩算法
实时写入 :支持高并发写入
丰富的数据类型和函数
Q2: ClickHouse的表引擎?
A:
MergeTree :最强大的引擎,支持分区、索引、副本
ReplacingMergeTree :去重,适合upsert场景
SummingMergeTree :预聚合,自动求和
AggregatingMergeTree :预聚合,支持多种聚合函数
CollapsingMergeTree :支持删除和更新
Distributed :分布式表,不存储数据
Q3: ClickHouse的存储组织结构?
A:
分区目录结构 :
目录命名:PartitionId_MinBlockNum_MaxBlockNum_Level
PartitionID :分区ID,如20210301
MinBlockNum/MaxBlockNum :分区块编号,合并时更新
Level :合并层级,合并次数越多层级越高
数据文件结构 :
primary.idx :主键索引文件,稀疏索引,每8192行一个索引项
[Column].bin :列数据文件,使用LZ4压缩
[Column].mrk2 :标记文件,记录bin文件中数据的偏移信息
minmax_[Column].idx :Min/Max索引,记录分区字段的最小最大值
partition.dat :分区文件,保存分区表达式生成的值
columns.txt :列信息文件
count.txt :计数文件,记录数据行数
checksums.txt :校验文件,校验文件完整性
索引查找过程 :
通过primary.idx定位到可能包含数据的索引粒度
通过.mrk2文件找到对应的数据块在.bin文件中的位置
读取.bin文件中的数据块
解压数据块,查找具体数据
一般问题
Q1: ClickHouse的索引机制?
A:
主键索引 :稀疏索引,每8192行一个索引项
二级索引(跳数索引) :
minmax:记录Min/Max值
set:记录去重值
ngrambf_v1:布隆过滤器
分区索引 :基于分区键的Min/Max索引
Q2: ClickHouse vs Doris的对比?
A:
ClickHouse优势 :
单表查询性能更好
导入速度更快
功能更丰富(更多表引擎、函数)
多租户管理更完善
Doris优势 :
使用更简单(SQL标准支持更好)
Join性能更好(多表查询)
运维更简单(扩缩容、故障恢复)
点查能力更强
对数据湖支持更好
Q3: ClickHouse的列式存储实现?
A:
列式存储原理 :
每列数据单独存储在一个文件中
同一列的数据在物理上连续
查询时只读取需要的列文件
数据压缩 :
默认使用LZ4压缩
支持多种压缩算法:ZSTD、ZLIB、Brotli等
列式存储压缩率高(同类型数据连续)
标记文件(.mrk2)作用 :
建立primary.idx和.bin文件之间的映射
包含三个信息:
Offset in compressed file:压缩数据块在bin文件中的偏移量
Offset in decompressed block:数据在解压块中的偏移量
Rows count:行数,通常等于index_granularity
数据文件(.bin)结构 :
Checksum(16字节):数据校验
Compression algorithm(1字节):压缩算法编号
Compressed size(4字节):压缩后大小
Decompressed size(4字节):解压后大小
Compressed data:压缩数据
困难问题
Q1: ClickHouse的MergeTree合并机制?
A:
后台线程定期合并小的数据块
合并时按主键排序,去重(ReplacingMergeTree)
合并策略:根据数据块大小和数量决定
可以通过OPTIMIZE手动触发合并
Q2: ClickHouse的分布式查询?
A:
使用Distributed表引擎
查询自动分发到各个分片
结果自动聚合
支持本地表和分布式表
Q3: ClickHouse的Distributed表引擎?
A:
Distributed表定义 :
分布式表是逻辑表,本身不存储数据
是本地表的访问代理,类似分库中间件
通过Distributed表可以访问多个数据分片
创建分布式表 :
1 2 3 4 5 6 CREATE TABLE distributed_table ON CLUSTER 'cluster_name' ( id UInt32, name String, date Date ) ENGINE = Distributed(cluster_name, database_name, local_table_name, sharding_key)
参数说明 :
cluster_name:集群名称
database_name:数据库名
local_table_name:本地表名
sharding_key:分片键(可选),用于数据分片
工作原理 :
写入时:根据sharding_key将数据分发到对应的分片
查询时:自动分发查询到各个分片,然后聚合结果
支持本地优先:可以设置prefer_localhost_replica优先查询本地副本
Q4: ClickHouse的ReplicatedMergeTree副本机制?
A:
ReplicatedMergeTree :
支持数据副本的表引擎
副本之间通过ZooKeeper实现数据一致性
提供高可用性和数据冗余
创建副本表 :
1 2 3 4 5 6 7 8 CREATE TABLE replicated_table ON CLUSTER 'cluster_name' ( id UInt32, name String, date Date ) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/table_name' , '{replica}' ) PARTITION BY date ORDER BY id
参数说明 :
第一个参数:ZooKeeper路径,{shard}会被替换为分片ID
第二个参数:副本标识,{replica}会被替换为副本名称
副本同步机制 :
通过ZooKeeper协调副本之间的数据同步
写入操作会同步到所有副本
合并操作由主副本执行,其他副本复制
支持自动故障恢复
ZooKeeper的作用 :
存储元数据(表结构、分区信息等)
协调副本之间的操作
实现分布式锁
监控副本状态
Q5: ClickHouse的ON CLUSTER语法?
A:
ON CLUSTER作用 :
在集群的所有节点上执行DDL操作
简化集群管理,无需在每个节点单独执行
SELECT语句也可以使用ON CLUSTER达到分布式查询的效果
支持的DDL操作 :
CREATE TABLE:创建表
DROP TABLE:删除表
ALTER TABLE:修改表结构
RENAME TABLE:重命名表
TRUNCATE TABLE:清空表
DDL示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 CREATE TABLE test_table ON CLUSTER 'my_cluster' ( id UInt32, name String ) ENGINE = MergeTree() ORDER BY id;CREATE TABLE replicated_table ON CLUSTER 'my_cluster' ( id UInt32, name String ) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/replicated_table' , '{replica}' ) ORDER BY id;DROP TABLE test_table ON CLUSTER 'my_cluster' ;ALTER TABLE test_table ON CLUSTER 'my_cluster' ADD COLUMN age UInt8;
SELECT … ON CLUSTER :
SELECT语句也可以使用ON CLUSTER在集群所有节点上执行
相当于查询分布式表,但不需要创建Distributed表引擎
查询会自动分发到各个分片,结果自动聚合
SELECT示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 SELECT * FROM local_table ON CLUSTER 'my_cluster' WHERE id = 1 ;SELECT date , count (* ) as cnt FROM local_table ON CLUSTER 'my_cluster' WHERE date >= '2024-01-01' GROUP BY date ORDER BY date ;SELECT * FROM distributed_table WHERE id = 1 ;
ON CLUSTER的优势 :
灵活性 :不需要创建Distributed表,直接查询本地表
简化管理 :避免维护分布式表定义
动态查询 :可以临时查询任意本地表
注意事项 :
需要配置集群信息(在config.xml或metrika.xml中)
确保所有节点都能访问ZooKeeper(如果使用副本)
DDL操作会在所有节点上执行,需要等待完成
SELECT … ON CLUSTER要求所有节点都有相同的表结构
查询性能与使用Distributed表类似
Q6: ClickHouse集群的数据分片策略?
A:
分片键(Sharding Key) :
在创建Distributed表时指定
用于决定数据写入哪个分片
可以使用hash函数、取模等方式
分片策略 :
随机分片 :
不指定sharding_key
数据随机分发到各个分片
适合数据均匀分布的场景
Hash分片 :
使用sharding_key = hash(id)
相同key的数据写入同一分片
适合需要按key聚合的场景
取模分片 :
使用sharding_key = id % shard_count
简单直接,但扩展性差
分片选择原则 :
数据均匀分布 :避免数据倾斜
查询本地化 :尽量让查询在本地完成
扩展性 :支持动态添加分片
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 CREATE TABLE distributed_table ON CLUSTER 'cluster_name' ( id UInt32, name String ) ENGINE = Distributed(cluster_name, database_name, local_table, intHash32(id)) CREATE TABLE distributed_table ON CLUSTER 'cluster_name' ( id UInt32, name String ) ENGINE = Distributed(cluster_name, database_name, local_table, rand())
Q7: ClickHouse MergeTree的合并策略详解?
A:
Tiered合并策略 :
相同大小的分区目录合并
合并后大小翻倍
类似LSM Tree的Size-Tiered Compaction
适合写密集型场景
问题:空间放大,需要更多存储空间
合并触发条件 :
分区目录数量达到阈值(默认10个)
分区目录大小达到阈值
手动触发:OPTIMIZE TABLE
合并过程 :
选择多个小的分区目录
按主键排序合并数据
生成新的分区目录,Level+1
删除旧的分区目录
优化 :
控制合并频率,避免影响写入
监控合并进度
合理设置分区策略,减少合并压力
使用max_bytes_to_merge_at_max_space_in_pool控制合并大小
Q8: ClickHouse集群的配置?
A:
集群配置方式 :
在config.xml或metrika.xml中配置
支持多个集群配置
配置示例(metrika.xml) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <yandex > <clickhouse_remote_servers > <my_cluster > <shard > <replica > <host > node1</host > <port > 9000</port > </replica > <replica > <host > node2</host > <port > 9000</port > </replica > </shard > <shard > <replica > <host > node3</host > <port > 9000</port > </replica > <replica > <host > node4</host > <port > 9000</port > </replica > </shard > </my_cluster > </clickhouse_remote_servers > </yandex >
配置说明 :
<shard>:定义一个分片
<replica>:定义分片的副本
每个分片可以有多个副本
副本之间通过ZooKeeper同步
ZooKeeper配置 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <zookeeper > <node > <host > zk1</host > <port > 2181</port > </node > <node > <host > zk2</host > <port > 2181</port > </node > <node > <host > zk3</host > <port > 2181</port > </node > </zookeeper >
Q9: ClickHouse集群的查询优化?
A:
查询分发策略 :
查询自动分发到各个分片
每个分片并行执行查询
结果自动聚合返回
本地优先查询 :
设置prefer_localhost_replica=1
优先查询本地副本,减少网络开销
适合副本查询场景
查询优化技巧 :
使用本地表 :
如果知道数据在哪个分片,直接查询本地表
避免分布式表的查询开销
使用SELECT … ON CLUSTER :
不需要创建Distributed表,直接查询本地表
查询会自动分发到所有节点并聚合结果
适合临时查询或不需要长期维护分布式表的场景
合理使用分片键 :
查询条件包含分片键,可以只查询对应分片
减少查询范围
避免跨分片JOIN :
尽量在同一个分片内JOIN
跨分片JOIN性能较差
使用物化视图 :
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 SELECT * FROM distributed_table WHERE id = 1 ;SELECT * FROM local_table ON CLUSTER 'my_cluster' WHERE id = 1 ;SELECT * FROM local_table WHERE id = 1 ;SET prefer_localhost_replica = 1 ;SELECT * FROM distributed_table WHERE id = 1 ;SELECT date , count (* ) as cnt, sum (amount) as total FROM local_table ON CLUSTER 'my_cluster' WHERE date >= '2024-01-01' GROUP BY date ORDER BY date ;
SELECT … ON CLUSTER vs Distributed表 :
特性
SELECT … ON CLUSTER
Distributed表
是否需要创建表
否
是
查询方式
直接查询本地表
查询分布式表
灵活性
高,可查询任意表
低,需要预先定义
维护成本
低
中等
性能
相同
相同
适用场景
临时查询、探索性查询
长期使用的查询
Q3: ClickHouse的分区目录合并机制?
A:
合并触发 :
后台线程定期检查需要合并的分区目录
合并策略:Tiered合并策略、Log Byte Size合并策略
可以通过OPTIMIZE手动触发合并
合并过程 :
选择多个小的分区目录
按主键排序合并数据
生成新的分区目录,Level+1
删除旧的分区目录
合并目的 :
减少分区目录数量,提高查询性能
删除标记为删除的数据(ReplacingMergeTree)
优化数据分布
合并优化 :
控制合并频率,避免影响写入
监控合并进度
合理设置分区策略,减少合并压力
Q4: ClickHouse的MergeTree与LSM Tree的关系?
A:
MergeTree的设计理念 :
ClickHouse文档中提到:”MergeTree这个名词是在我们耳熟能详的LSM Tree之上做减法而来——去掉了MemTable和Log”
MergeTree是LSM Tree的简化版本,专为OLAP场景优化
与LSM Tree的相似点 :
写入优化 :数据直接写入磁盘文件,顺序写入
后台合并 :后台线程定期合并小的数据块
分层存储 :数据按Level分层,Level越高数据越老
不可变性 :数据文件一旦写入不可修改
与LSM Tree的区别 :
无MemTable :数据直接写入磁盘,不需要内存缓冲
无WAL :不需要Write-Ahead Log
列式存储 :数据按列存储,而非按行存储
适合OLAP :专为分析查询优化,而非点查询
MergeTree的合并机制 :
类似LSM Tree的Compaction
使用Tiered合并策略:相同大小的数据块合并
合并时按主键排序,去重(ReplacingMergeTree)
Level表示合并次数,Level越高数据越老
优势 :
简化设计 :去掉MemTable和WAL,降低复杂度
适合批量导入 :OLAP场景下批量导入数据
列式存储 :压缩率高,查询性能好
Q5: ClickHouse的集群架构?
A:
多主架构 :
ClickHouse采用多主(无中心)架构
集群中的每个节点角色对等
客户端访问任意一个节点都能得到相同的效果
不同于Elasticsearch、HDFS的主从架构
分片(Shard) :
分片将数据进行横向切分
每个分片对应一个服务节点
1个分片只能对应1个服务节点
分片数量上限取决于节点数量
副本(Replica) :
支持数据副本,提高可用性
副本概念与Elasticsearch类似
分片是逻辑概念,物理承载由副本承担
副本之间通过ZooKeeper实现数据一致性
本地表和分布式表 :
本地表 :等同于一个数据分片,存储实际数据
分布式表 :逻辑表,不存储数据,是本地表的访问代理
分布式表类似分库中间件,代理访问多个数据分片
3.3 Elasticsearch 基础问题
Q1: Elasticsearch的核心概念?
A:
Index :索引,类似数据库
Type :类型,类似表(7.x后已废弃)
Document :文档,类似行记录
Field :字段,类似列
Shard :分片,数据切分单位
Replica :副本,高可用保证
Q2: Elasticsearch的倒排索引?
A:
Term Dictionary :词项字典,存储所有词项
Posting List :倒排列表,记录包含该词项的文档ID
Term Index :词项索引,使用FST压缩,快速定位Term
优化 :使用Frame of Reference和Roaring Bitmaps压缩Posting List
Q3: Lucene的Segment机制?
A:
Segment定义 :Segment是Lucene索引的基本单位,由域信息、词信息、标准化因子、删除文档等信息组成
不可变性 :Segment一旦形成就无法修改,具有一次写入、多次读取的特点
删除机制 :删除文档时,不会修改Segment文件,而是将删除信息存储到单独的文件中
多Segment :一个索引由多个Segment组成,查询时需要查询所有Segment并合并结果
Segment合并 :后台线程定期合并小Segment为大Segment,提高查询性能
Q4: Elasticsearch/Lucene的Segment文件结构?
A:
Segment文件组成 :
.si文件 :Segment信息文件,包含Segment元数据
.cfs/.cfe文件 :复合文件,包含所有索引文件(可选)
倒排索引文件 :
.tim:Term Dictionary和Posting List
.tip:Term Index(FST)
.doc:Posting List(文档ID和词频)
.pos:位置信息
.pay:payload信息
正排索引文件 :
.fdt:存储文档的字段数据
.fdx:字段数据索引
其他文件 :
.dvm/.dvd:DocValues(列式存储,用于排序和聚合)
.nvd/.nvm:归一化因子
.liv:删除文档列表
文件作用 :
倒排索引 :用于全文搜索,快速定位包含某个词的文档
正排索引 :用于根据docId获取文档内容
DocValues :列式存储,用于排序、聚合、脚本执行
一般问题
Q1: Elasticsearch的搜索流程?
A:
查询解析:解析查询语句
路由:根据routing确定分片
分片查询:在各个分片上执行查询
结果合并:合并各分片结果
排序打分:计算相关度分数
返回结果
Q2: Elasticsearch的写入流程?
A:
写入内存缓冲区(IndexWriter Buffer)
定期refresh:将缓冲区数据写入新segment,打开segment使其可搜索
写入translog:保证数据不丢失
定期flush:将segment刷盘,清空translog
Q3: Lucene的索引构建过程?
A:
1. 文档分析(Analysis) :
使用Analyzer对文档进行分词
不同Field可以指定不同的Analyzer
包括:分词、去停用词、大小写转换、词干提取等
2. Term索引构建 :
字符关键词检索 :
Term Index:树形结构,记录Term Dictionary的前缀offset
Term Dictionary:存储所有词项
使用FST(有限状态转换器)压缩Term Dictionary到内存
数值关键词检索 :
使用BKDTree(K-D树和B+树的结合)
支持高效的数值范围查询和多维查询
可以局部更新
3. Posting List构建 :
Posting List必须有序(按docId排序)
使用Frame of Reference压缩:增量编码,将大数变小数
使用Roaring Bitmaps压缩:使用(id/65535, id%65535)格式存储
支持SkipList快速查找docId
4. 写入Segment :
多线程并发写入,每个线程有独立的DocumentsWriterPerThread
数据处理完成后,触发FlushPolicy判定
写入新的Segment文件
Q4: Elasticsearch的Refresh原理及表现?
A:
Refresh原理 :
触发机制 :
默认每1秒自动执行一次refresh
可通过index.refresh_interval配置(默认1s,可设置为-1禁用自动refresh)
可通过API手动触发:POST /index/_refresh
执行过程 :
将内存缓冲区(IndexWriter Buffer)中的数据写入新的segment文件
新segment写入文件系统缓存(Page Cache),但不执行fsync
打开新segment,使其可以被搜索
清空内存缓冲区,准备接收新的数据
与Flush的区别 :
Refresh :数据写入Page Cache,不刷盘,速度快,数据可能丢失
Flush :数据刷盘(fsync),数据持久化,速度慢,但数据安全
Refresh的表现 :
近实时搜索 :
数据写入后,默认最多1秒后可以被搜索到
这是”近实时”而非”实时”的原因
可以通过手动refresh实现立即搜索:POST /index/_refresh
性能影响 :
Refresh会创建新的segment,频繁refresh会产生大量小segment
小segment过多会影响查询性能(需要查询多个segment)
频繁refresh会增加CPU和I/O开销
优化策略 :
写入场景 :可以增大refresh间隔(如30s),减少refresh频率,提高写入性能
搜索场景 :可以减小refresh间隔(如100ms),提高搜索实时性
批量导入 :可以临时禁用refresh(index.refresh_interval: -1),导入完成后恢复
实际表现 :
写入后立即查询可能查不到(数据还在内存缓冲区)
等待1秒后可以查询到(refresh后)
手动refresh后立即可以查询到
数据在Page Cache中,如果服务器宕机可能丢失(需要translog恢复)
示例配置 :
1 2 3 4 5 6 PUT /my_index/_settings { "index" : { "refresh_interval" : "30s" } }
困难问题
Q1: Elasticsearch的分布式原理?
A:
使用分片(Shard)实现水平扩展
主分片负责写入,副本分片负责读取
使用一致性哈希分配文档到分片
支持动态调整分片和副本数量
Q2: Elasticsearch的性能优化?
A:
索引优化 :合理设置分片数和副本数
查询优化 :使用filter代替query、避免深度分页
写入优化 :批量写入、调整refresh间隔
硬件优化 :SSD、足够内存、JVM调优
Q6: Lucene的倒排合并算法?
A:
合并场景 :
多个Term的Posting List需要合并(AND查询)
多个Term的Posting List需要取并集(OR查询)
合并算法(AND查询) :
在termA的Posting List开始遍历,得到第一个元素docId=1
Set currentDocId=1
在termB的Posting List中search(currentDocId),返回大于等于currentDocId的docId
如果返回的docId等于currentDocId,说明两个Term都包含该文档,加入结果
如果返回的docId大于currentDocId,更新currentDocId,继续查找
重复步骤3-5,直到某个Posting List遍历完
优化 :
使用SkipList加速查找
优先遍历短的Posting List
使用位运算优化密集数据
Q7: Lucene的打分机制(TF-IDF)?
A:
TF-IDF公式 :
TF(Term Frequency) :词频,词在文档中出现的次数
IDF(Inverse Document Frequency) :逆文档频率,衡量词的稀有程度
Score = TF × IDF
TF计算 :
词在文档中出现的频率
通常使用归一化的TF:tf(t,d) = count(t,d) / totalTerms(d)
或者使用对数TF:tf(t,d) = 1 + log(count(t,d))
IDF计算 :
idf(t) = log(N / df(t))
N:文档总数
df(t):包含词t的文档数
词越稀有,IDF越大
其他因素 :
字段长度归一化 :短文档的TF可能被高估
字段权重 :不同字段的重要性不同
查询提升 :某些查询词的重要性更高
ES中的改进 :
BM25算法:改进的TF-IDF,更好地处理字段长度
支持自定义打分函数
支持Function Score Query自定义打分逻辑
Q8: Elasticsearch的存储文件组织?
A:
索引目录结构 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 index_name/ ├── _0/ # 分片0 │ ├── segments_N # Segment元数据 │ ├── write.lock # 写入锁 │ └── [segment_name]/ │ ├── .si # Segment信息 │ ├── .cfs/.cfe # 复合文件(可选) │ ├── .tim # Term Dictionary和Posting List │ ├── .tip # Term Index(FST) │ ├── .doc # Posting List │ ├── .fdt # 字段数据 │ ├── .fdx # 字段数据索引 │ ├── .dvd # DocValues数据 │ └── .dvm # DocValues元数据 └── _1/ # 分片1
文件存储特点 :
不可变性 :Segment文件一旦写入不可修改
压缩存储 :使用压缩算法减少存储空间
分离存储 :倒排索引和正排索引分离存储
列式存储 :DocValues使用列式存储
存储优化 :
使用合适的压缩算法(LZ4、DEFLATE)
定期合并Segment,减少文件数量
合理设置分片数,避免小文件过多
使用SSD提高I/O性能
Q3: Lucene的FST(有限状态转换器)原理?
A:
FST的定义 :
Finite State Transducer,一种类似Trie树的有限状态机
既能判断key是否存在,还能给出对应的output(Posting List的offset)
FST的优势 :
共享前缀和后缀 :相比Trie树,FST还能共享后缀,进一步压缩空间
时间优化 :O(len(key))时间复杂度查找
空间优化 :在时间和空间复杂度上都做了最大优化
内存加载 :可以将Term Dictionary完全加载到内存,快速定位Term
FST的结构 :
节点表示状态
边表示字符转换
每个路径对应一个Term
路径终点存储该Term对应的Posting List的offset
应用 :
Lucene使用FST构建Term Index
快速定位Term在Term Dictionary中的位置
然后顺序查找Term Dictionary找到对应的Posting List
Q4: Lucene的Posting List压缩算法?
A:
1. Frame of Reference(FOR) :
原理 :增量编码压缩,将大数变小数
方法 :存储相邻docId的差值,而不是绝对docId
示例 :[100, 101, 103, 110] → [100, 1, 2, 7]
优势 :差值通常很小,可以用更少的字节存储
2. Roaring Bitmaps :
原理 :将docId分成高16位和低16位
格式 :(id/65535, id%65535)
存储 :高16位作为key,低16位作为bitmap
优势 :
稀疏数据用数组存储
密集数据用bitmap存储
自动选择最优存储方式
3. SkipList(跳表) :
用途 :快速查找Posting List中的docId
特点 :
元素有序(按docId排序)
跳跃有固定间隔
多层级结构
优势 :O(log n)时间复杂度查找
Q5: Elasticsearch的DocValues(正排索引)存储结构?
A:
DocValues定义 :
列式存储结构,与倒排索引相反
按文档ID顺序存储字段值
用于排序、聚合、脚本执行
存储文件 :
.dvd文件 :存储DocValues数据
.dvm文件 :DocValues元数据
DocValues类型 :
Numeric DocValues :数值类型,使用压缩存储
Binary DocValues :二进制类型,如字符串
Sorted DocValues :排序的DocValues,用于文本字段
SortedSet DocValues :多值字段
应用场景 :
排序(Sort):需要按字段值排序
聚合(Aggregation):需要统计字段值
脚本执行:需要在脚本中访问字段值
高基数字段:不适合倒排索引的字段
与倒排索引的区别 :
倒排索引 :词 → 文档ID列表,用于搜索
DocValues :文档ID → 字段值,用于排序和聚合
两者互补,共同支持搜索和分析功能
Q6: Elasticsearch的Nested类型?
A:
Nested类型定义 :
用于处理对象数组中的独立对象
每个嵌套对象被索引为独立的文档
保持对象之间的独立性
为什么需要Nested类型 :
对象数组的问题 :默认情况下,对象数组会被扁平化(flattened)
数据丢失 :对象之间的关系会丢失
查询不准确 :无法精确匹配对象数组中的特定对象
示例问题 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { "user" : "张三" , "tags" : [ { "name" : "java" , "value" : "高级" } , { "name" : "python" , "value" : "中级" } ] }
Nested类型解决 :
每个嵌套对象作为独立文档索引
保持对象内部字段的关联性
可以精确查询嵌套对象
Q7: Elasticsearch的Nested类型使用?
A:
创建Nested字段 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 PUT /my_index { "mappings" : { "properties" : { "user" : { "type" : "keyword" } , "tags" : { "type" : "nested" , "properties" : { "name" : { "type" : "keyword" } , "value" : { "type" : "keyword" } } } } } }
插入数据 :
1 2 3 4 5 6 7 8 PUT /my_index/_doc/1 { "user" : "张三" , "tags" : [ { "name" : "java" , "value" : "高级" } , { "name" : "python" , "value" : "中级" } ] }
Nested查询 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 GET /my_index/_search { "query" : { "nested" : { "path" : "tags" , "query" : { "bool" : { "must" : [ { "term" : { "tags.name" : "java" } } , { "term" : { "tags.value" : "高级" } } ] } } } } } GET /my_index/_search { "query" : { "bool" : { "must" : [ { "nested" : { "path" : "tags" , "query" : { "term" : { "tags.name" : "java" } } } } , { "nested" : { "path" : "tags" , "query" : { "term" : { "tags.value" : "高级" } } } } ] } } }
Nested聚合 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 GET /my_index/_search { "aggs" : { "tags_agg" : { "nested" : { "path" : "tags" } , "aggs" : { "name_terms" : { "terms" : { "field" : "tags.name" } } } } } }
Q8: Elasticsearch的Nested类型应用场景?
A:
适用场景 :
一对多关系 :
需要精确匹配对象数组中的对象 :
需要聚合嵌套对象 :
示例场景 :
1. 电商订单 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 { "order_id" : "12345" , "customer" : "张三" , "items" : [ { "product" : "手机" , "price" : 5000 , "quantity" : 1 } , { "product" : "耳机" , "price" : 200 , "quantity" : 2 } ] } { "nested" : { "path" : "items" , "query" : { "bool" : { "must" : [ { "term" : { "items.product" : "手机" } } , { "range" : { "items.price" : { "gt" : 4000 } } } ] } } } }
2. 用户标签 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 { "user_id" : "1001" , "name" : "张三" , "tags" : [ { "category" : "技能" , "name" : "Java" , "level" : "高级" } , { "category" : "技能" , "name" : "Python" , "level" : "中级" } , { "category" : "兴趣" , "name" : "阅读" , "level" : "初级" } ] } { "nested" : { "path" : "tags" , "query" : { "bool" : { "must" : [ { "term" : { "tags.category" : "技能" } } , { "term" : { "tags.name" : "Java" } } , { "term" : { "tags.level" : "高级" } } ] } } } }
3. 文章评论 :
1 2 3 4 5 6 7 8 { "article_id" : "100" , "title" : "Elasticsearch教程" , "comments" : [ { "user" : "用户A" , "content" : "很好" , "rating" : 5 } , { "user" : "用户B" , "content" : "不错" , "rating" : 4 } ] }
Q9: Nested类型的性能考虑?
A:
性能特点 :
存储开销 :每个嵌套对象作为独立文档存储,增加存储空间
查询性能 :需要查询多个嵌套文档,性能略低于普通查询
索引性能 :需要为每个嵌套对象创建文档,索引速度较慢
优化策略 :
合理使用 :
只在需要精确匹配时使用Nested
嵌套对象数量不宜过多(建议<100个)
查询优化 :
使用inner_hits获取匹配的嵌套对象
避免深度嵌套查询
替代方案 :
如果不需要精确匹配,使用普通对象数组
考虑使用join类型(父子文档)
示例(inner_hits) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 GET /my_index/_search { "query" : { "nested" : { "path" : "tags" , "query" : { "term" : { "tags.name" : "java" } } , "inner_hits" : { "size" : 10 } } } }
Q10: Nested类型 vs Join类型?
A:
Nested类型 :
关系 :同一文档内的对象数组
存储 :嵌套对象作为隐藏文档存储在同一分片
查询 :使用nested查询
适用场景 :一对多关系,对象数量较少(<100)
Join类型 :
关系 :父子文档关系,可以跨文档
存储 :父子文档存储在同一分片
查询 :使用has_parent/has_child查询
适用场景 :父子关系,子文档数量很大
对比 :
特性
Nested
Join
关系类型
同一文档内
跨文档
查询性能
较快
较慢
存储开销
中等
较大
适用场景
对象数组
父子关系
文档数量
较少(<100)
可以很多
选择建议 :
对象数组,需要精确匹配 :使用Nested
父子关系,子文档很多 :使用Join
简单数组,不需要精确匹配 :使用普通对象数组
Q6: Elasticsearch的Refresh与Segment合并的关系?
A:
Refresh产生Segment :
每次refresh都会创建一个新的segment
Segment是不可变的,写入后不能修改
频繁refresh会产生大量小segment
Segment合并(Merge) :
后台线程定期合并小segment为大segment
合并策略:Tiered合并策略、Log Byte Size合并策略
合并目的:
减少segment数量,提高查询性能
删除已删除的文档(标记为deleted)
优化索引结构
Refresh与Merge的平衡 :
问题 :
Refresh太频繁 → 产生大量小segment → 查询性能下降
Refresh太慢 → 搜索延迟高 → 用户体验差
小segment多 → Merge压力大 → 影响写入性能
优化策略 :
写入密集型 :增大refresh间隔(30s-60s),减少segment产生
搜索密集型 :减小refresh间隔(100ms-1s),提高实时性
混合场景 :使用默认1s,根据实际监控调整
批量导入 :禁用refresh,导入完成后手动refresh一次
监控指标 :
indices.segments.count:segment数量
indices.segments.memory_in_bytes:segment内存占用
indices.refresh.total:refresh总次数
indices.merges.total:merge总次数
最佳实践 :
监控segment数量,保持在合理范围(如每GB数据100-200个segment)
定期执行force merge:POST /index/_forcemerge?max_num_segments=1
根据业务特点调整refresh策略
使用索引模板统一配置
Q9: Elasticsearch的Segment合并与LSM Tree的关系?
A:
设计理念 :
Elasticsearch的Segment合并机制借鉴了LSM Tree的思想
但针对全文搜索场景做了优化
与LSM Tree的相似点 :
写入优化 :数据先写入内存缓冲区,批量刷盘
后台合并 :后台线程定期合并小的Segment
不可变性 :Segment一旦写入不可修改
分层合并 :使用Tiered合并策略,类似Size-Tiered Compaction
与LSM Tree的区别 :
无MemTable :使用内存缓冲区(IndexWriter Buffer),而非MemTable
无多Level :Segment不按Level分层,而是按大小和时间合并
倒排索引 :存储的是倒排索引,而非KV数据
Refresh机制 :有Refresh机制,使数据近实时可搜索
Segment合并策略 :
Tiered合并策略 :类似Size-Tiered Compaction
相同大小的Segment合并
合并后大小翻倍
适合写密集型场景
Log Byte Size合并策略 :类似Leveled Compaction
优化 :
控制Refresh频率,减少Segment产生
选择合适的合并策略
监控合并进度,避免影响查询性能
四、数据仓库与数据模型 4.1 数据仓库理论 基础问题
Q1: 数据仓库的分层架构?
A:
ODS(操作数据层) :原始数据,与源系统保持一致
DWD(明细数据层) :清洗、整合后的明细数据
DWS(汇总数据层) :轻度汇总,面向分析主题
ADS(应用数据层) :面向应用的数据集市
DIM(维度层) :维度表,相对静态
Q2: 维度建模(星型模型、雪花模型)?
A:
星型模型 :事实表在中心,维度表围绕,维度表不规范化
雪花模型 :维度表规范化,减少冗余但增加JOIN
选择原则 :星型模型查询性能更好,雪花模型存储更省
一般问题
Q1: 事实表和维度表?
A:
事实表 :存储业务度量值,如订单金额、数量
维度表 :存储描述性属性,如商品信息、用户信息
事实表类型 :
事务事实表:记录业务事件
快照事实表:记录某个时间点的状态
累积快照事实表:记录过程性事件
Q2: 缓慢变化维(SCD)?
A:
Type 1 :覆盖,不保留历史
Type 2 :新增行,保留历史(常用)
Type 3 :新增列,保留有限历史
选择 :根据业务需求选择合适类型
困难问题
Q1: 数据仓库的ETL设计?
A:
Extract :从源系统提取数据
Transform :数据清洗、转换、整合
Load :加载到目标系统
增量处理 :只处理变更数据,提高效率
错误处理 :异常数据记录和处理
Q2: 数据仓库的元数据管理?
A:
技术元数据 :表结构、字段类型、数据源信息
业务元数据 :业务含义、数据字典、业务规则
操作元数据 :ETL任务、数据质量、血缘关系
管理工具 :Atlas、DataHub等
4.2 ETL与数据治理 基础问题
Q1: 常用的ETL工具?
A:
DataX :阿里开源,支持多种数据源
Kettle(Pentaho) :开源ETL工具
Flink CDC :基于Flink的实时数据同步
Sqoop :Hadoop生态的数据导入导出工具
Canal :基于MySQL binlog的数据同步
Q2: 数据质量保障(DQC)?
A:
完整性 :数据不缺失
准确性 :数据正确无误
一致性 :数据逻辑一致
及时性 :数据及时更新
唯一性 :数据不重复
有效性 :数据符合业务规则
一般问题
Q1: 数据血缘分析?
A:
追踪数据的来源和去向
用于影响分析、问题排查、数据治理
实现方式:解析SQL、记录元数据、构建血缘图
Q2: 数据标准化的实践?
A:
统一命名规范
统一数据类型和格式
统一业务规则
建立数据字典
定期审查和更新
困难问题
Q1: 实时数据仓库架构?
A:
Lambda架构 :批处理和流处理并行
Kappa架构 :统一流处理
实时数仓 :Flink + Kafka + OLAP
数据一致性 :最终一致性、对账机制
Q2: 数据治理体系?
A:
组织架构 :数据治理委员会、数据Owner
制度规范 :数据标准、数据质量规范
技术平台 :元数据管理、数据质量、数据安全
流程机制 :数据申请、审批、使用流程
五、中间件与分布式系统 5.1 Redis 基础问题
Q1: Redis的数据结构?
A:
String :字符串,SDS实现
List :列表,双向链表或压缩列表
Hash :哈希,字典或压缩列表
Set :集合,整数集合或字典
ZSet :有序集合,跳跃表+字典
BitMap :位图
HyperLogLog :基数统计
Stream :流,类似Kafka
Q2: Redis的持久化机制?
A:
RDB :快照,定期保存数据
AOF :追加日志,记录写操作
混合持久化 :RDB+AOF,结合两者优点
一般问题
Q1: Redis的集群模式?
A:
主从复制 :一主多从,读写分离
哨兵模式 :监控主节点,自动故障转移
Cluster模式 :分片集群,无中心架构
16384个slot,分配到各个节点
使用gossip协议通信
支持动态扩缩容
Q2: Redis的缓存问题?
A:
缓存穿透 :查询不存在的数据
缓存击穿 :热点key过期
缓存雪崩 :大量key同时过期
困难问题
Q1: Redis的内存淘汰策略?
A:
noeviction :不淘汰,内存满时写入失败
allkeys-lru :所有key中最近最少使用
allkeys-random :所有key中随机
volatile-lru :设置了过期时间的key中最近最少使用
volatile-random :设置了过期时间的key中随机
volatile-ttl :设置了过期时间的key中即将过期
Q2: Redis的分布式锁实现?
A:
使用SET命令的NX和EX选项
设置过期时间防止死锁
使用Lua脚本保证原子性
考虑锁续期问题
使用Redisson等成熟方案
5.2 MySQL 基础问题
Q1: MySQL的索引原理?
A:
B+树索引 :InnoDB默认索引
非叶子节点只存key,叶子节点存数据
支持范围查询和排序
聚簇索引:数据存储在索引中
非聚簇索引:索引指向数据位置
最左前缀原则 :联合索引从左到右匹配
索引优化 :覆盖索引、索引下推
Q2: MySQL的事务隔离级别?
A:
Read Uncommitted :读未提交,可能脏读
Read Committed :读已提交,避免脏读
Repeatable Read :可重复读,避免不可重复读(MySQL默认)
Serializable :串行化,避免幻读
一般问题
Q1: MySQL的MVCC?
A:
多版本并发控制,实现非锁定读
通过undo log和ReadView实现
每行记录有隐藏字段:事务ID、回滚指针
ReadView判断数据版本对当前事务的可见性
Q2: MySQL的锁机制?
A:
行锁 :锁定单行,InnoDB支持
表锁 :锁定整张表
间隙锁 :锁定索引记录之间的间隙
Next-Key Lock :行锁+间隙锁,解决幻读
困难问题
Q1: MySQL的索引优化?
A:
索引选择 :区分度高的列、经常查询的列
索引设计 :避免过多索引、考虑最左前缀
索引失效 :函数、类型转换、NULL值
覆盖索引 :索引包含查询所需的所有列
Q2: MySQL的主从复制原理?
A:
Master将变更写入binlog
Slave的IO线程拉取binlog
Slave的SQL线程重放binlog
支持异步、半同步、全同步复制
主从延迟问题及优化
Q3: MySQL的Online DDL原理?
A:
Online DDL定义 :
MySQL 5.6+支持在线DDL操作
在DDL执行期间允许DML操作(读写)
减少锁表时间,提高可用性
执行方式 :
ALGORITHM=INPLACE :原地修改,不重建表
ALGORITHM=COPY :复制表,需要重建表
LOCK=NONE :允许并发读写
LOCK=SHARED :允许读,禁止写
LOCK=EXCLUSIVE :禁止读写
执行阶段 :
Prepare阶段 :
创建临时frm文件
持有EXCLUSIVE-MDL锁(短暂)
确定执行方式(copy/rebuild/no-rebuild)
更新数据字典
分配row_log对象(rebuild类型)
DDL执行阶段 :
降级MDL锁,允许读写
扫描原表数据,构造新索引
记录DDL期间的增量操作(row_log)
重放row_log到新表
Commit阶段 :
升级到EXCLUSIVE-MDL锁(短暂)
重做最后一部分增量
更新数据字典
提交事务,rename文件
一般问题
Q3: Online DDL的支持情况?
A:
支持INPLACE且允许并发DML的操作 :
添加/删除二级索引
修改列名(数据类型不变)
修改列默认值
修改自增值
添加/删除外键约束
支持INPLACE但需要重建表的操作 :
添加/删除列
修改列顺序
修改列NULL/NOT NULL属性
添加/删除主键
修改ROW_FORMAT
OPTIMIZE TABLE
不支持INPLACE的操作(必须COPY) :
修改列数据类型
删除主键(未同时添加新主键)
变更表字符集
注意事项 :
Prepare和Commit阶段会短暂锁表
大表DDL仍然耗时较长
主从复制场景下,从库会延迟
Q4: Online DDL vs pt-online-schema-change vs gh-ost?
A:
MySQL原生Online DDL :
优点 :
MySQL内置支持,无需额外工具
操作简单,直接执行ALTER TABLE
对触发器无影响
缺点 :
Prepare和Commit阶段仍会短暂锁表
大表操作耗时较长
主从延迟问题
pt-online-schema-change(pt-osc) :
原理 :
创建新表(带新结构)
创建触发器(INSERT/UPDATE/DELETE)
分批拷贝数据到新表
重命名表完成切换
优点 :
全程不锁表(除最后rename)
可以控制拷贝速度
可以暂停和恢复
缺点 :
需要触发器支持
表上有触发器时不能使用
需要额外的磁盘空间
主从延迟仍然存在
gh-ost :
原理 :
基于binlog的Online DDL工具
作为伪装的备库,读取binlog
在主库上创建ghost表
拷贝数据+应用binlog增量
优点 :
无触发器 :不依赖触发器
轻量级 :对主库影响小
可暂停 :可以暂停和恢复
可测试 :支持测试模式
动态可控 :可以动态调整参数
可审计 :可以查看进度和状态
缺点 :
需要binlog为ROW格式
需要额外的工具部署
学习成本较高
对比总结 :
特性
Online DDL
pt-osc
gh-ost
锁表时间
短暂(Prepare/Commit)
最后rename
最后cut-over
触发器
无影响
需要触发器
不需要
binlog格式
无要求
无要求
需要ROW格式
可暂停
否
是
是
可测试
否
否
是
主从延迟
有
有
有
使用复杂度
低
中
高
适用场景
简单DDL操作
复杂DDL操作
生产环境DDL
选择建议 :
简单操作 (添加索引、修改列名):使用Online DDL
复杂操作 (添加列、修改类型):使用pt-osc或gh-ost
生产环境 :优先使用gh-ost(更安全、可控)
有触发器 :使用gh-ost或Online DDL
困难问题
Q3: Online DDL的row_log机制?
A:
row_log作用 :
记录DDL执行期间产生的DML操作
保证数据一致性
只在rebuild类型操作时使用
工作原理 :
记录增量 :DDL执行期间,所有DML操作记录到row_log
应用增量 :DDL完成后,将row_log中的操作应用到新表
保证一致性 :确保DDL前后的数据一致
实现细节 :
row_log是一个循环缓冲区
记录INSERT、UPDATE、DELETE操作
在Commit阶段重放最后一部分增量
保证数据完整性
Q4: Online DDL的性能优化?
A:
优化策略 :
选择合适的算法 :
优先使用ALGORITHM=INPLACE
避免ALGORITHM=COPY(会锁表)
控制锁级别 :
使用LOCK=NONE允许并发DML
避免LOCK=EXCLUSIVE(完全锁表)
分批操作 :
大表操作考虑分批执行
使用pt-osc或gh-ost控制速度
业务低峰期 :
监控和调整 :
监控DDL执行进度
使用gh-ost可以动态调整参数
主从延迟处理 :
考虑先在从库执行,再切换
使用pt-osc控制延迟时间
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ALTER TABLE table_name ADD INDEX idx_name (column_name),ALGORITHM= INPLACE, LOCK= NONE ; ALTER TABLE table_name ADD COLUMN new_col INT ,ALGORITHM= INPLACE, LOCK= NONE ; ALTER TABLE table_name MODIFY COLUMN col_name VARCHAR (100 ), ALGORITHM= COPY , LOCK= SHARED;
5.3 微服务 基础问题
Q1: 微服务的服务治理?
A:
服务发现 :Nacos、Eureka、Consul
负载均衡 :Ribbon、Nginx
熔断降级 :Sentinel、Hystrix
限流 :令牌桶、漏桶算法
分布式事务 :Seata、TCC、Saga
配置中心 :Nacos、Apollo
一般问题
Q1: 分布式事务的解决方案?
A:
2PC(两阶段提交) :强一致性,但性能差
TCC(Try-Confirm-Cancel) :补偿型事务
Saga :长事务,最终一致性
Seata :AT模式,自动回滚
消息事务 :基于消息队列的最终一致性
困难问题
Q1: 服务网格(Service Mesh)?
A:
将服务治理功能从业务代码中分离
通过Sidecar代理实现
支持多语言、多协议
代表:Istio、Linkerd
Q2: 分布式系统的CAP理论?
A:
Consistency :一致性
Availability :可用性
Partition tolerance :分区容错性
三者只能同时满足两个
实际系统需要权衡
5.4 领域驱动设计(DDD) 基础问题
Q1: DDD的核心概念?
A:
DDD定义 :
Domain-Driven Design,领域驱动设计
由Eric Evans在2003年提出
一种软件设计方法论,强调业务领域建模
核心概念 :
领域(Domain) :业务领域,软件要解决的问题域
子领域(Subdomain) :领域的细分,分为核心域、支撑域、通用域
限界上下文(Bounded Context) :明确的边界,领域模型的适用范围
实体(Entity) :有唯一标识的对象
值对象(Value Object) :没有唯一标识,通过属性值判断相等
聚合(Aggregate) :一组相关对象的集合,有聚合根
领域服务(Domain Service) :不属于实体或值对象的领域逻辑
领域事件(Domain Event) :领域内发生的重要事件
Q2: 实体(Entity)和值对象(Value Object)的区别?
A:
实体(Entity) :
有唯一标识(ID)
通过ID判断相等性
生命周期内标识不变
可以修改属性
示例:User(userId)、Order(orderId)
值对象(Value Object) :
没有唯一标识
通过属性值判断相等性
不可变(Immutable)
可以替换整个对象
示例:Money(amount + currency)、Address(street + city)
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class User { private Long userId; private String name; } public class Money { private BigDecimal amount; private String currency; }
Q3: 聚合(Aggregate)和聚合根(Aggregate Root)?
A:
聚合定义 :
一组相关对象的集合
有明确的边界
通过聚合根访问内部对象
保证数据一致性
聚合根(Aggregate Root) :
聚合的入口点
外部只能通过聚合根访问聚合
负责维护聚合的一致性
有唯一标识
聚合设计原则 :
一致性边界 :聚合内保证强一致性,聚合间最终一致性
小聚合 :聚合应该尽可能小
通过ID引用 :聚合间通过ID引用,不直接引用对象
事务边界 :一个事务只能修改一个聚合
示例 :
1 2 3 4 5 6 7 8 9 10 11 public class Order { private Long orderId; private List<OrderItem> items; private Address address; public void addItem (Product product, int quantity) { } }
一般问题
Q1: 限界上下文(Bounded Context)?
A:
限界上下文定义 :
明确的边界,领域模型的适用范围
一个限界上下文对应一个领域模型
不同限界上下文可以有不同的模型
设计原则 :
明确边界 :清晰定义上下文边界
独立模型 :每个上下文有自己的领域模型
上下文映射 :定义上下文之间的关系
避免大泥球 :不要将所有内容放在一个上下文中
上下文映射模式 :
共享内核(Shared Kernel) :共享部分模型
客户-供应商(Customer-Supplier) :上游下游关系
遵奉者(Conformist) :完全遵循上游模型
防腐层(Anti-Corruption Layer) :隔离外部系统
发布语言(Published Language) :通过事件或API通信
Q2: 领域服务(Domain Service)和应用服务(Application Service)的区别?
A:
领域服务(Domain Service) :
包含领域逻辑
不属于实体或值对象
无状态
示例:转账服务、价格计算服务
应用服务(Application Service) :
协调领域对象完成用例
不包含业务逻辑
调用领域服务、领域对象
处理事务、权限等横切关注点
示例:订单服务、用户服务
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class TransferService { public void transfer (Account from, Account to, Money amount) { if (from.getBalance().isLessThan(amount)) { throw new InsufficientBalanceException (); } from.debit(amount); to.credit(amount); } } public class OrderApplicationService { private OrderRepository orderRepository; private TransferService transferService; public void createOrder (CreateOrderCommand cmd) { Order order = new Order (cmd.getItems()); orderRepository.save(order); transferService.transfer(...); } }
Q3: 领域事件(Domain Event)?
A:
领域事件定义 :
领域内发生的重要事件
表示业务事实
用于解耦和集成
事件特点 :
不可变 :事件一旦发生不可修改
命名清晰 :使用过去时,如OrderCreated
包含上下文 :包含事件发生时的上下文信息
发布订阅 :通过事件总线发布和订阅
使用场景 :
解耦 :解耦不同聚合
集成 :不同限界上下文之间的集成
审计 :记录业务操作历史
CQRS :命令查询职责分离
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class OrderCreatedEvent { private Long orderId; private Long userId; private LocalDateTime occurredAt; } public class Order { private List<DomainEvent> domainEvents = new ArrayList <>(); public void confirm () { this .status = OrderStatus.CONFIRMED; domainEvents.add(new OrderConfirmedEvent (this .orderId)); } }
困难问题
Q1: DDD的分层架构?
A:
DDD分层架构 :
用户接口层(User Interface Layer) :
处理用户交互
展示数据
接收用户输入
示例:Controller、DTO
应用层(Application Layer) :
协调领域对象
处理用例
事务管理
示例:Application Service、Command/Query
领域层(Domain Layer) :
核心业务逻辑
实体、值对象、聚合
领域服务、领域事件
示例:Entity、Value Object、Domain Service
基础设施层(Infrastructure Layer) :
技术实现
数据持久化
消息队列、缓存等
示例:Repository实现、消息发送
依赖方向 :
上层依赖下层
领域层不依赖其他层(核心)
基础设施层实现领域层的接口
示例 :
1 2 3 4 5 6 7 User Interface Layer (Controller) ↓ Application Layer (Application Service) ↓ Domain Layer (Entity, Domain Service) ↑ Infrastructure Layer (Repository实现)
Q2: CQRS(命令查询职责分离)?
A:
CQRS定义 :
Command Query Responsibility Segregation
将命令(写操作)和查询(读操作)分离
使用不同的模型和存储
CQRS架构 :
命令端(Command Side) :
查询端(Query Side) :
优势 :
性能优化 :读写分离,独立优化
模型简化 :命令模型和查询模型可以不同
扩展性 :可以独立扩展读写端
适用场景 :
读写比例差异大
查询需求复杂
需要高性能查询
事件溯源场景
Q3: 事件溯源(Event Sourcing)?
A:
事件溯源定义 :
不存储当前状态,存储事件流
通过重放事件重建状态
事件是不可变的
工作原理 :
业务操作产生事件
事件存储到事件存储(Event Store)
通过重放事件重建聚合状态
可以查询历史任意时间点的状态
优势 :
完整历史 :保留所有历史记录
审计 :天然支持审计
时间旅行 :可以查询历史状态
调试 :可以重放事件调试问题
挑战 :
事件版本 :事件结构可能变化
性能 :重建状态需要重放事件
快照 :需要定期创建快照优化性能
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class AccountCreatedEvent { ... }public class MoneyDepositedEvent { ... }public class MoneyWithdrawnEvent { ... }public class Account { public Account (List<DomainEvent> events) { for (DomainEvent event : events) { apply(event); } } private void apply (DomainEvent event) { if (event instanceof MoneyDepositedEvent) { this .balance = this .balance.add(...); } } }
Q4: DDD在微服务架构中的应用?
A:
微服务与DDD的关系 :
微服务边界应该对应限界上下文
一个微服务对应一个或多个限界上下文
DDD帮助识别微服务边界
设计原则 :
限界上下文即服务边界 :
一个限界上下文对应一个微服务
避免跨服务的领域模型共享
通过API通信 :
领域事件集成 :
独立数据存储 :
实践建议 :
识别限界上下文 :通过业务分析识别
定义服务边界 :限界上下文就是服务边界
事件驱动 :使用领域事件实现服务集成
API设计 :设计清晰的API契约
数据一致性 :接受最终一致性
示例 :
1 2 3 4 5 6 7 8 9 10 订单服务(Order Service) - 限界上下文:订单上下文 - 聚合:Order、OrderItem - 发布事件:OrderCreated、OrderPaid 支付服务(Payment Service) - 限界上下文:支付上下文 - 聚合:Payment - 订阅事件:OrderCreated - 发布事件:PaymentCompleted
六、项目经验与领域知识 6.1 项目深挖 基础问题
Q1: 请介绍一个你参与的大数据项目?
回答要点:
项目背景 :业务需求、数据规模
技术选型 :使用的技术栈
核心功能 :主要实现的功能
个人贡献 :你在项目中的角色和贡献
一般问题
Q1: 请介绍一个你主导或深度参与的大数据平台项目?
回答要点:
项目背景 :业务需求、数据规模、技术挑战
技术选型 :为什么选择这些技术栈
架构设计 :整体架构、模块划分、数据流
核心功能 :数据采集、存储、计算、服务化
遇到的挑战 :
数据倾斜问题及解决方案
性能优化(查询优化、资源调优)
稳定性保障(监控、告警、故障恢复)
项目成果 :性能提升、成本降低、业务价值
Q2: 如何保障大数据任务的稳定性?
A:
任务监控 :实时监控任务状态、资源使用
基线设置 :设置任务完成时间基线
SLA保障 :定义服务等级协议
故障恢复 :
告警机制 :及时发现问题
困难问题
Q1: 如何保障数据处理的准确性和时效性?
A:
准确性 :
数据校验规则
数据质量监控
对账机制
异常数据告警
时效性 :
实时计算(Flink)
增量处理
任务优先级调度
资源保障
Q2: 大数据平台的监控体系设计?
A:
指标监控 :任务执行时间、资源使用率、数据量
日志监控 :错误日志、异常日志
告警机制 :阈值告警、趋势告警
可视化 :Dashboard、报表
工具 :Prometheus、Grafana、ELK
6.2 性能优化 基础问题
Q1: 什么是数据倾斜?
A:
数据分布不均匀,某些key的数据量远大于其他key
导致某些Task处理时间过长,影响整体性能
常见场景:group by、join、distinct
一般问题
Q1: 如何定位和解决数据倾斜问题?
A:
定位 :
通过监控发现某些Task执行时间过长
查看数据分布,找出热点key
解决 :
Flink :加随机前缀、LocalKeyBy、Rebalance
Spark :加随机前缀、自定义分区器、增加并行度
Hive :MapJoin、空值处理、倾斜数据单独处理
业务层面 :打散热点key、预聚合
Q2: 如何进行性能调优?
A:
资源调优 :
CPU、内存、网络、磁盘
合理设置并行度
调整JVM参数
算法优化 :
使用更高效的算法
减少Shuffle
使用广播变量
存储优化 :
查询优化 :
困难问题
Q1: 大规模数据处理的性能优化策略?
A:
数据分区 :合理分区减少扫描数据量
索引优化 :建立合适的索引
物化视图 :预计算常用查询
缓存策略 :热点数据缓存
并行处理 :充分利用集群资源
算法优化 :选择合适的数据结构和算法
Q2: 实时计算系统的性能优化?
A:
背压处理 :合理设置缓冲区大小
状态优化 :使用RocksDB存储大状态
Checkpoint优化 :调整Checkpoint间隔和超时
资源调优 :合理设置并行度和资源
算子优化 :减少不必要的计算和网络传输
七、运维与工程能力 7.1 Linux与部署 基础问题
Q1: 常用的Linux命令?
A:
文件操作 :ls、cd、mkdir、rm、cp、mv
文本处理 :cat、grep、awk、sed、tail、head
进程管理 :ps、top、kill、nohup
网络 :netstat、ss、ping、curl
权限 :chmod、chown、sudo
压缩 :tar、zip、unzip
一般问题
Q1: 如何排查性能问题?
A:
CPU :top、htop、vmstat、pidstat
内存 :free、vmstat、jmap
磁盘I/O :iostat、iotop
网络 :netstat、ss、iftop、tcpdump
日志分析 :grep、awk、sed
Q2: Docker和K8s的了解?
A:
Docker :容器化技术,镜像、容器、仓库
K8s :容器编排,Pod、Service、Deployment
使用场景 :应用部署、资源隔离、弹性伸缩
困难问题
Q1: 如何设计一个高可用的部署方案?
A:
多副本部署 :避免单点故障
负载均衡 :分散请求压力
健康检查 :自动剔除故障节点
故障转移 :自动切换到备用节点
监控告警 :及时发现问题
Q2: 容器化部署的实践?
A:
镜像构建 :Dockerfile编写、多阶段构建
资源限制 :CPU、内存限制
网络配置 :容器网络、服务发现
存储管理 :数据卷、持久化存储
编排工具 :K8s、Docker Compose
7.2 开发工具链 基础问题
Q1: Git的使用?
A:
分支管理 :master、develop、feature、hotfix
常用命令 :add、commit、push、pull、merge、rebase
冲突解决 :merge冲突、rebase冲突
工作流 :Git Flow、GitHub Flow
一般问题
Q1: Maven的使用?
A:
依赖管理 :pom.xml、仓库、坐标
生命周期 :clean、compile、test、package、install、deploy
插件 :编译插件、打包插件
困难问题
Q1: CI/CD流程设计?
A:
持续集成 :代码提交触发构建和测试
持续部署 :自动化部署到测试/生产环境
工具 :Jenkins、GitLab CI、GitHub Actions
流程 :代码检查、单元测试、集成测试、部署
八、前沿技术与软素质 8.1 大数据与AI结合 基础问题
Q1: 大模型与数据平台的结合?
A:
RAG架构 :检索增强生成,结合向量数据库
Agent应用 :智能数据分析、自动SQL生成
向量数据库 :Milvus、Pinecone,用于相似度检索
Prompt工程 :优化提示词,提升模型效果
Q2: 什么是RAG(检索增强生成)?
A:
定义 :Retrieval-Augmented Generation,结合检索和生成的技术
原理 :
将知识库文档向量化存储到向量数据库
用户查询时,先检索相关文档
将检索到的文档作为上下文,与大模型一起生成答案
优势 :
减少模型幻觉
支持知识更新(无需重新训练模型)
可追溯答案来源
应用场景 :智能问答、文档检索、知识库查询
Q3: AI编程工具的使用?
A:
Cursor、通义灵码 :代码生成、代码补全
使用场景 :SQL生成、代码重构、文档生成
提升效率 :减少重复工作,专注业务逻辑
一般问题
Q1: Spring AI的核心概念和使用?
A:
核心概念 :
ChatClient :统一的聊天客户端接口,支持多种模型(OpenAI、Anthropic、Ollama等)
PromptTemplate :提示词模板,支持变量替换
VectorStore :向量存储接口,支持多种向量数据库
EmbeddingModel :文本向量化模型
Function Calling :函数调用能力,让模型可以调用外部工具
基本使用 :
1 2 3 4 5 6 7 8 @Autowired private ChatClient chatClient;public String chat (String userMessage) { Prompt prompt = new Prompt (userMessage); ChatResponse response = chatClient.call(prompt); return response.getResult().getOutput().getContent(); }
优势 :
统一的API接口,切换模型无需修改代码
与Spring生态深度集成
支持流式响应、函数调用等高级特性
支持RAG、Agent等复杂场景
Q2: LangChain4J的使用场景?
A:
核心功能 :
链式调用(Chain) :将多个组件串联,实现复杂流程
工具调用(Tools) :让模型可以调用外部API、数据库等
记忆管理(Memory) :管理对话历史、上下文
文档加载器(Document Loaders) :从各种数据源加载文档
文本分割(Text Splitters) :将长文档分割成chunk
典型应用场景 :
RAG应用 :
1 2 3 4 DocumentLoader loader = new FileSystemDocumentLoader ();EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel ();EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore <>();
Agent应用 :
1 2 3 4 5 Agent agent = Agent.builder() .tools(calculator, databaseTool) .chatLanguageModel(chatModel) .build();
数据平台集成 :
Q3: RAG架构的完整实现流程?
A:
1. 文档预处理 :
文档加载:从文件系统、数据库、API等加载文档
文本分割:将长文档分割成小的chunk(通常512-1024 tokens)
元数据提取:提取文档标题、作者、时间等信息
2. 向量化存储 :
使用Embedding模型将文本转换为向量
存储到向量数据库(Milvus、Pinecone、Elasticsearch等)
同时存储原始文本和元数据
3. 检索阶段 :
用户查询向量化
在向量数据库中检索相似文档(Top-K)
可以使用混合检索:向量检索 + 关键词检索
4. 生成阶段 :
将检索到的文档作为上下文
构建Prompt:系统提示词 + 检索文档 + 用户问题
调用大模型生成答案
5. 优化策略 :
重排序(Rerank) :对检索结果重新排序,提高相关性
多轮对话 :维护对话历史,支持上下文理解
引用溯源 :返回答案来源,提高可信度
Q4: MCP(Model Context Protocol)是什么?
A:
定义 :
Model Context Protocol,模型上下文协议
由Anthropic提出的标准协议,用于连接AI应用和外部数据源
核心概念 :
Server :提供数据或服务的服务器(如数据库、API、文件系统)
Client :AI应用客户端
Tools :服务器提供的工具(如查询数据库、读取文件)
Resources :服务器提供的资源(如数据库表、文件)
优势 :
标准化 :统一的协议,不同工具可以互操作
安全性 :明确的权限控制,只暴露必要的工具
可扩展 :易于添加新的数据源和服务
类型安全 :使用JSON Schema定义工具和资源
应用场景 :
连接数据库,让AI可以查询数据
连接API,让AI可以调用外部服务
连接文件系统,让AI可以读取文档
构建AI Agent,实现复杂任务自动化
示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { "tools" : [ { "name" : "query_database" , "description" : "查询数据库" , "inputSchema" : { "type" : "object" , "properties" : { "sql" : { "type" : "string" } } } } ] }
困难问题
Q1: 如何将AI能力集成到数据平台?
A:
数据准备 :数据清洗、特征工程
模型训练 :使用大数据平台进行分布式训练
模型部署 :模型服务化、A/B测试
效果评估 :模型性能监控、持续优化
Q2: Dify平台的核心功能和使用?
A:
核心功能 :
工作流编排 :
可视化拖拽式工作流设计
支持条件分支、循环、并行执行
支持多种节点类型:LLM、知识库检索、代码执行、API调用等
知识库管理 :
支持多种文档格式(PDF、Word、Markdown等)
自动文档分割和向量化
支持多种向量数据库
文档更新和版本管理
Agent构建 :
工具调用:支持函数调用、API调用
记忆管理:对话历史、长期记忆
推理能力:支持ReAct、Plan-and-Execute等模式
模型管理 :
支持多种模型提供商(OpenAI、Anthropic、本地模型等)
模型切换和A/B测试
成本控制和监控
典型应用场景 :
智能数据分析助手 :
用户用自然语言提问
Agent理解问题,生成SQL查询
执行查询,分析结果
用自然语言返回分析结果
文档问答系统 :
上传企业内部文档
构建知识库
用户提问,RAG检索相关文档
生成答案并标注来源
数据平台集成 :
连接数据平台API
提供自然语言查询接口
自动生成报表和分析
技术架构 :
前端 :React + TypeScript
后端 :Python FastAPI
向量数据库 :支持Milvus、Qdrant、Weaviate等
模型服务 :支持OpenAI、Anthropic、本地模型等
部署 :支持Docker、Kubernetes部署
Q3: RAG系统的性能优化策略?
A:
1. 检索优化 :
混合检索 :向量检索 + BM25关键词检索,提高召回率
重排序 :使用Cross-Encoder对检索结果重新排序
检索策略 :Top-K检索、阈值过滤、多样性采样
2. 向量化优化 :
模型选择 :选择适合领域的Embedding模型
批量处理 :批量向量化,提高效率
缓存机制 :缓存常用查询的向量
3. 文档处理优化 :
智能分割 :按语义分割,而非简单按长度
重叠窗口 :chunk之间保留重叠,避免语义截断
元数据过滤 :利用元数据快速过滤不相关文档
4. 生成优化 :
Prompt优化 :设计清晰的Prompt模板
上下文压缩 :只保留最相关的文档片段
流式生成 :支持流式响应,提升用户体验
5. 系统优化 :
缓存策略 :缓存常见问题的答案
异步处理 :检索和生成异步执行
负载均衡 :多实例部署,提高并发能力
监控告警 :监控检索质量、生成质量、响应时间
Q4: 如何设计一个企业级AI Agent系统?
A:
1. 架构设计 :
Agent核心 :LLM + 工具调用 + 记忆管理
工具层 :数据库工具、API工具、文件工具等
知识层 :向量数据库、知识图谱
服务层 :API网关、认证授权、监控告警
2. 工具设计 :
标准化接口 :统一的工具调用接口
权限控制 :细粒度的权限管理
错误处理 :工具调用失败的重试和降级
日志记录 :完整的工具调用日志
3. 记忆管理 :
短期记忆 :对话历史,存储在内存或Redis
长期记忆 :重要信息,存储到向量数据库
记忆检索 :根据当前对话检索相关历史
记忆更新 :定期更新和清理过期记忆
4. 安全控制 :
输入验证 :防止注入攻击、恶意输入
输出过滤 :过滤敏感信息、不当内容
访问控制 :基于角色的权限管理
审计日志 :记录所有操作,便于审计
5. 性能优化 :
并发控制 :限制并发请求数
超时控制 :设置合理的超时时间
缓存策略 :缓存常见查询结果
负载均衡 :多实例部署,提高可用性
6. 监控运维 :
指标监控 :请求量、响应时间、错误率
质量监控 :答案质量、用户满意度
成本监控 :API调用成本、资源消耗
告警机制 :异常情况及时告警
8.2 学习与沟通能力 基础问题
Q1: 最近关注或学习什么新技术?
回答要点:
说明学习的新技术及其背景
学习方法和过程
实际应用场景或实践
学习收获和思考
一般问题
Q1: 如何与产品、前端、测试、运维协作?
回答要点:
需求理解 :与产品充分沟通,理解业务需求
接口设计 :与前端协商接口规范
测试配合 :提供测试数据、环境支持
运维协作 :提供部署文档、监控指标
问题处理 :及时响应、快速定位、有效沟通
困难问题
Q1: 如何推动技术方案落地?
A:
技术选型 :充分调研,对比优缺点
方案设计 :考虑可扩展性、可维护性
团队沟通 :技术分享、方案评审
风险控制 :灰度发布、回滚方案
效果评估 :数据监控、持续优化
九、SQL能力考察 9.1 复杂SQL编写 基础问题
Q1: 窗口函数的使用?
示例:
1 2 3 4 5 6 7 8 9 10 11 SELECT user_id, order_date, order_amount, SUM (order_amount) OVER ( PARTITION BY user_id ORDER BY order_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS cumulative_amount FROM orders;
一般问题
Q1: 多表关联查询?
要点:
选择合适的JOIN类型(INNER、LEFT、RIGHT、FULL)
注意NULL值处理
使用合适的ON条件
避免笛卡尔积
Q2: 性能优化SQL?
要点:
使用索引:WHERE条件使用索引列
避免全表扫描:合理使用WHERE、LIMIT
减少子查询:使用JOIN替代
使用EXPLAIN分析执行计划
困难问题
Q1: 复杂业务SQL编写?
示例:计算每个用户最近30天的订单金额,并按金额排序取前10
1 2 3 4 5 6 7 8 SELECT user_id, SUM (order_amount) AS total_amount FROM ordersWHERE order_date >= DATE_SUB(CURRENT_DATE , INTERVAL 30 DAY )GROUP BY user_idORDER BY total_amount DESC LIMIT 10 ;
十、算法题考核 10.1 Easy - 数据流中的Top K元素 题目描述 : 设计一个数据结构,能够实时统计数据流中出现频率最高的K个元素。
示例 :
1 2 输入: [1, 1, 1, 2, 2, 3], K = 2 输出: [1, 2]
思路 :
使用HashMap统计每个元素的频率
使用最小堆(大小为K)维护Top K元素
当新元素到来时,更新频率,调整堆
代码实现 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import java.util.*;public class TopKFrequent { private Map<Integer, Integer> freq; private PriorityQueue<Map.Entry<Integer, Integer>> minHeap; private int k; public TopKFrequent (int k) { this .k = k; this .freq = new HashMap <>(); this .minHeap = new PriorityQueue <>((a, b) -> a.getValue() - b.getValue()); } public void add (int num) { freq.put(num, freq.getOrDefault(num, 0 ) + 1 ); for (Map.Entry<Integer, Integer> entry : freq.entrySet()) { if (minHeap.size() < k) { minHeap.offer(entry); } else if (entry.getValue() > minHeap.peek().getValue()) { minHeap.poll(); minHeap.offer(entry); } } } public List<Integer> getTopK () { List<Integer> result = new ArrayList <>(); while (!minHeap.isEmpty()) { result.add(minHeap.poll().getKey()); } Collections.reverse(result); return result; } }
大数据场景应用 :
实时统计热门商品、热门搜索词
流式数据处理中的Top K查询
监控系统中的异常检测
10.2 Medium - 数据分片与负载均衡 题目描述 : 设计一个一致性哈希算法,实现数据分片和负载均衡。给定N个数据节点和M个数据key,将key均匀分配到节点上,并支持节点的动态添加和删除。
示例 :
1 2 3 节点: ["node1", "node2", "node3"] Key: ["key1", "key2", "key3", "key4", "key5"] 要求: 将key均匀分配到节点,并支持节点动态变化
思路 :
使用一致性哈希环,将节点和key都映射到环上
每个key顺时针找到第一个节点
使用虚拟节点解决负载不均衡问题
节点变化时,只影响相邻节点的数据
代码实现 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 import java.util.*;public class ConsistentHash { private TreeMap<Long, String> ring; private int virtualNodes; private List<String> nodes; public ConsistentHash (List<String> nodes, int virtualNodes) { this .nodes = new ArrayList <>(nodes); this .virtualNodes = virtualNodes; this .ring = new TreeMap <>(); for (String node : nodes) { addNode(node); } } private void addNode (String node) { for (int i = 0 ; i < virtualNodes; i++) { String virtualNode = node + "#" + i; long hash = hash(virtualNode); ring.put(hash, node); } } public void removeNode (String node) { for (int i = 0 ; i < virtualNodes; i++) { String virtualNode = node + "#" + i; long hash = hash(virtualNode); ring.remove(hash); } nodes.remove(node); } public String getNode (String key) { if (ring.isEmpty()) { return null ; } long hash = hash(key); Map.Entry<Long, String> entry = ring.ceilingEntry(hash); if (entry == null ) { entry = ring.firstEntry(); } return entry.getValue(); } private long hash (String key) { return key.hashCode(); } public Map<String, List<String>> getMigrationData (String removedNode) { Map<String, List<String>> migration = new HashMap <>(); for (String node : nodes) { if (!node.equals(removedNode)) { migration.put(node, new ArrayList <>()); } } return migration; } }
大数据场景应用 :
分布式存储系统的数据分片(如HDFS、Cassandra)
缓存系统的负载均衡(如Redis Cluster)
分布式计算的任务分配
10.3 Hard - 流式数据的中位数计算 题目描述 : 设计一个数据结构,能够实时计算数据流的中位数。数据流中会不断有新的数字加入,需要随时能够返回当前的中位数。
示例 :
1 2 3 4 5 输入: [1, 2, 3, 4, 5] 中位数: 3 输入: [1, 2, 3, 4, 5, 6] 中位数: (3 + 4) / 2 = 3.5
思路 :
使用两个堆:最大堆存储较小的一半,最小堆存储较大的一半
保证两个堆的大小差不超过1
最大堆的堆顶 <= 最小堆的堆顶
中位数 = 最大堆堆顶(奇数)或两个堆顶的平均值(偶数)
代码实现 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 import java.util.*;public class MedianFinder { private PriorityQueue<Integer> maxHeap; private PriorityQueue<Integer> minHeap; public MedianFinder () { maxHeap = new PriorityQueue <>((a, b) -> b - a); minHeap = new PriorityQueue <>(); } public void addNum (int num) { maxHeap.offer(num); minHeap.offer(maxHeap.poll()); if (maxHeap.size() < minHeap.size()) { maxHeap.offer(minHeap.poll()); } } public double findMedian () { if (maxHeap.size() > minHeap.size()) { return maxHeap.peek(); } else { return (maxHeap.peek() + minHeap.peek()) / 2.0 ; } } public void removeNum (int num) { if (maxHeap.contains(num)) { maxHeap.remove(num); if (maxHeap.size() < minHeap.size()) { maxHeap.offer(minHeap.poll()); } } else if (minHeap.contains(num)) { minHeap.remove(num); if (minHeap.size() < maxHeap.size() - 1 ) { minHeap.offer(maxHeap.poll()); } } } public class SlidingWindowMedian { private Deque<Integer> window; private MedianFinder finder; private int windowSize; public SlidingWindowMedian (int k) { this .windowSize = k; this .window = new ArrayDeque <>(); this .finder = new MedianFinder (); } public void addNum (int num) { if (window.size() == windowSize) { int removed = window.pollFirst(); finder.removeNum(removed); } window.offerLast(num); finder.addNum(num); } public double getMedian () { return finder.findMedian(); } } }
大数据场景应用 :
实时监控系统中的指标中位数计算
流式数据分析中的统计指标
时间序列数据的滑动窗口中位数
性能监控中的延迟中位数统计
优化考虑 :
对于超大规模数据流,可以使用近似算法(如Count-Min Sketch)
支持分布式计算,多个节点分别计算,最后合并
考虑数据过期机制,只保留最近N个数据
十一、总结 大数据应用开发岗位需要掌握:
扎实的Java基础 :集合、并发、JVM
大数据技术栈 :Flink、Spark、Kafka、Hive、Hadoop
OLAP数据库 :Doris、ClickHouse
数据仓库理论 :分层架构、维度建模
中间件 :Redis、MySQL、消息队列
项目经验 :实际项目经验、问题解决能力
工程能力 :Linux、Docker、Git
学习能力 :持续学习新技术
算法能力 :数据结构、算法设计、大数据场景应用
面试时要注意:
结合项目经验回答问题
展示问题解决思路
体现技术深度和广度
展现学习能力和沟通能力
算法题要结合大数据场景思考