0%

全屋智能能做什么

按照递进,五个层级为:

一、控制灯光

把普通开关换成智能开关,比如:

  • 语音开灯、关灯
  • 手机远程控制
  • 一键全开 / 全关
  • 走到某个地方自动亮灯,离开后自动关灯
  • 起夜自动亮小灯

二、控制窗帘、窗户、遮阳

装智能窗帘电机,比如:

  • 早上自动开帘、晚上自动关帘
  • 配合灯光、空调一起联动

三、控制家电(空调、电视、风扇、扫地机)

  • 空调:智能空调控制器 / VRF 控制器
  • 电视 / 风扇:智能插座 / 红外遥控
  • 扫地机、加湿器、热水器等统一控制

比如:回家自动开空调、离家自动关电器

四、场景联动与自动化

常用场景:

  • 回家模式:开门 → 灯亮、窗帘开、空调开
  • 离家模式:关门 → 全关灯光、电器、开启安防
  • 睡眠模式:说”我睡了” → 关灯、关帘、空调调睡眠
  • 观影模式:灯变暗、窗帘拉上、电视打开

五、智能助手

家庭智能中枢可以通过摄像头观察并分析人的意图,实现 AI 自动控制全屋电器


全屋智能要做哪些事情

前期规划

1. 选择生态

选择有:米家、华为、苹果 HomeKit、Home Assistant 等

需要知道现在市面上的不同生态各自的产品能做到哪些功能,计算大概预算,选择一个生态。

2. 确定需求

  • 基础功能:灯光、窗帘、空调、门锁
  • 进阶功能:安防、语音控制、设备联动、自动化处理

根据选择的生态,结合自己生活习惯,确定全屋智能需要满足的功能。

3. 确定全屋电路改造方案

开关在哪、灯几路、窗帘电机位置、插座位置、有无吊顶、是否要做无主灯

水电改造

毛坯要设计电位,精装要改点位。主要要考虑:

  1. 智能开关:必须留零线,双控改无线,不用留双控线
  2. 窗帘盒:留插座(窗帘电机、梦幻帘、纱帘)
  3. 空调 / 新风:预留红外网关或 VRF 控制器
  4. 安防:门口摄像头、门磁、人体传感器
  5. 中控屏 / 语音音箱:预留电源和网口
  6. 全屋网络线路:需要根据全屋组网方案进行网线规划
  7. 上下水相关:零冷水、扫地机、洗地机等智能设备会对上下水有要求,需要规划水路改造

设备采购与安装调试

网络

全屋智能的第一步是全屋组网。组网方案有很多种,需要根据户型以及实际需求确定。

方案 优点 缺点 适用场景
AC+AP 稳定、美观 需提前布线 大户型,可用吸顶AP或AP面板
Mesh 路由 简单好扩展 节点间有线回程效果更好 多数家庭
无线中继 不用布线 网速一般,稳定性一般 租房或临时方案

灯光

如果是华为、苹果还好说,价格贵但品控好。

如果是米家生态,采购灯光时要注意:

  1. 不同品牌的灯颜色可能不一样,如果注重美观要尽量采购同品牌灯
  2. 不同位置要用不同的灯,比如射灯、筒灯、感应灯面对的场景都不同

智能开关

控制灯光,支持手机、语音、场景调节。

智能开关对全屋智能非常重要。尤其是你不是所有设备都能联网的情况,需要智能开关进行补充控制。智能开关同样可以绑定智能场景,作为全屋控制的核心途径。

💡 建议选择 4 开、带自定义屏显的开关,有足够的扩展性

其他设备

  • 网关:全屋智能大脑,负责设备联网与联动
  • 窗帘电机:自动开合窗帘,可定时、联动控制
  • 智能门锁:指纹 / 密码 / 手机开锁,联动回家场景
  • 传感器:人体感应、门窗开合、环境监测,触发自动化
  • 智能插座 / 空调控制:让传统家电变智能,远程开关、调温
  • 中控屏 / 音箱:语音控制 + 面板操作,全屋交互中心
  • 安防设备:监控、报警,保障居家安全

配置与后续优化

初始配置

  1. 开关、灯具、窗帘电机安装
  2. 设备入网、统一命名
  3. 设置智能场景:回家、离家、睡眠、观影、起夜
  4. 联动调试:
    • 人来灯亮、人走灯灭
    • 开门自动开灯、开空调
  5. 语音控制测试

入住后优化

  • 调整传感器灵敏度
  • 优化自动化逻辑
  • 逐步增加设备

普通人做智能家居的难点

1. 前期不懂规划

  • 没预留零线、网线、插座,很多智能设备装不了
  • 开关、窗帘、门锁、传感器位置乱选,用起来反人类

2. 生态割裂

小米、华为、HomeKit、涂鸦互不兼容,买一堆设备只能单独控制,不能联动。

3. 稳定性差

  • 无线设备多了就延迟、掉线、误触
  • 断网就瘫痪,半夜灯自己亮、窗帘自己开
  • 普通用户根本不会排查网络 / 网关问题

4. 学习成本高

  • 要懂:网关、WiFi / 蓝牙 / Zigbee / Thread、自动化场景、网络
  • 要折腾:固件升级、兼容调试、场景逻辑

5. 预算容易失控

  • 盲目扩展设备,东西越买越多
  • 便宜的不稳定,稳定的不便宜

6. 售后与维修麻烦

  • 坏一个设备,整屋联动可能失效
  • 普通电工不会修智能设备,出问题只能自己排查或找远程客服

问一

有几个比较疑惑的点:
1. 现在都说算法方向技术迭代很快,个人长期积累不太乐观,再加上 AI 编程发展迅速,结合目前行业裁员、晋升的实际情况,你怎么看待互联网行业的职业发展路径?有没有比较长期的发展方向?
2. 还有一种说法是先做几年算法,再往业务方向转,这种路径可行性高吗?如果可行,有哪些比较合适的方向,比如产品经理、互联网出海业务这些?

答一

我大概总结了一下你的问题,就是AI对未来的职业有什么影响、怎么看待个人技术积累、懂AI但不做AI去做业务行不行、有什么长期发展方向,我下面按顺序回答你:

AI编程对什么级别的程序员影响最大:
AI编程的发展路径大致是:和大模型问答输出代码片段 -〉cursor这种使用ai功能深度改造的idea -〉claude code这种纯vibe工具 。 三种方案我都比较深度的使用了下。从去年开始我在同事圈子小范围推广claude code+glm模型的方案,涵盖程序开发、数据开发、运维、产品,所有人给了非常正向的反馈,有工程能力不是很强的同事,甚至觉得即使没有其他程序开发同学支持,他也可以自己完成很多事情了。
根据我的观察和判断,我觉得即使以目前的ai水平,ai也可以在某些工作上替代初级开发人员和中级开发人员。唯有高级开发人受影响比较小,原因是:

  1. llm存在的幻觉、难以理解复杂意图、全局理解能力极差等固有缺陷
  2. ai编程最大的现实瓶颈是token的限制。面对复杂场景,ai需要大量的上下文进行决策,而ai在某些方面缺失了人进行深度思考并压缩上下文的能力,导致ai的决策总是不完美的。
  3. ai的架构设计能力不足。架构设计往往是因地制宜的,既需要考虑历史包袱,也需要考虑现实环境制约,还需要考虑人的因素。尤其是面对跨模块问题,ai目前能力还不足。
  4. ai生成代码,局部质量高,但从全局看,扩展性、可维护性有问题,高级程序员能引导ai进行重构以及规整代码
  5. 对复杂架构下程序bug的诊断,ai不如人

技术迭代与个人积累:
现在IT行业任何方向的技术迭代都很快,算法方向尤其快。但任何方向都需要长期积累才能形成突破。AI的出现,会挤压初级人才以及中级人才的生存空间,但人的学习曲线,又注定了要先经历初级和中级才能达到高级。我觉得这个矛盾未来可能会作为企业人才工作的主线,即如何将没有潜力的初级、中级人才的工作交给AI,转而将资源尽可能的投入有成长为高级人才潜力的人。因此,对于你而言:

  1. 积累是必须的,不积累的人更容易被AI轻松淘汰
  2. 要学会使用ai加速你的积累速度,做到比别人更快的积累知识
  3. 相比起单纯的积累,等到你毕业的时候,可能重要的是证明你是值得长期培养的,有成为高级人才潜力的人。方法有很多:表现出更高的智力水平、更积极的探索前沿技术的心态、比其他人更快的积累速度

先说我关于“业务”的理解:
无论你是做算法,还是工程,还是产品经理抑或是数据开发,AI几乎把人拉齐到了同一个赛道。

  • 对于不懂llm算法的人:开源的大模型足够解决绝大部分问题
  • 对于不懂程序开发和其他算法的人:AI能够快速构建应用程序,以及传统算法模型
  • 对不懂产品的人:未来基础产品思维,可能会成为IT行业要求的基础能力
  • 对不懂数据的人:目前大数据行业正在快速被AI重构,分析和理解数据的成本越来越低
    随着AI的能力越来越强,未来IT行业就业市场能容纳最多的人才的方向,我觉得是:能够利用AI提升业务的人,即能够充分的理解业务,然后使用已有的技术工具提升业务指标的人。
    比如:
  1. 懂业务流程、能用AI把链路效率拉满的人。清楚公司从获客、转化、交付、售后的每一环,知道哪里最痛、哪里最耗人,然后用大模型、自动化工具把重复劳动、人工判断、低效流程直接砍掉,把业务效率、人效比直接拉高
  2. 懂行业规则、能把AI落地成真实业务价值的人,比如金融行业懂风控与合规,能知道哪些环节可以使用AI,哪些环节必须保留人工
    这个能力不一定需要你有突破前沿技术的能力,在更多的场景下,需要的是你对业务的理解能力、知识的广度、对工具的熟悉程度、对前沿技术的了解等等。而且,这个能力在所有行业都会有一席之地,除了互联网行业,传统行业的数字化转型、IT建设等等都会需要这种人才。对于这个大的方向,最难的反而是对业务的理解能力,纯粹的程序员往往不如一线业务人员,新员工不如老员工,专才不如通才。AI正在消灭“纯技术执行岗”,但会放大“懂业务、会用 AI、能解决真实问题的人”。

长期的发展方向:
综上所属,结合你的实际情况,我觉得有两个发展方向:

  1. llm算法方向。如果AI是社会变革的最大推力,那么构建AI的那批人肯定能吃到最多的时代福利。目前,几乎只有Transformer架构还在迅猛成长,其他算法领域我觉得希望也不大,但也不排除未来AGI在真正实现时会抛弃transformer架构。走llm算法方向,只要第一份工作找好,那基本未来吃穿不愁。但这个方向竞争巨大,只有那些知识积累速度非常快的人才能站稳脚跟,你需要仔细评估你的个人能力
  2. 做一个通才,算法方向最为核心发展,但工程、产品、数据领域你也要懂一点,在招聘的时候瞄准企业类似“AI应用工程师”的岗位,直接去企业做AI业务。你的算法背景可以使你在技术层面获得一定的优势,但你的劣势是需要迅速熟悉业务,以及提升软实力(人际交往、项目推进等)。

现在要怎么做:
如果你选方向1,那你按你目前的规划进行即可。
如果选方向2,那么要做以下微调:

  1. 不能局限于llm算法。要学会程序开发,也要了解大数据。你有统计学背景,了解大数据对你来说不难
  2. 一定要学会借助用AI提高你的效率以及能力。比如AI编程工具。不会用AI提升自己的人,也无法用AI去提升业务。
  3. 要有意识的提高沟通能力、人际交往能力
  4. 要学会面试技巧,如我前文所言的,要让人觉得你有培养潜力,而不是被认为是一个可以被AI替代的员工

问二

走方向2,但这又让我产生了两个问题
1.大厂螺丝化问题,既然是走这个算法应用方向,前期肯定是奔着大厂去的,不过我从网上获取的信息经常提及了大厂螺丝化的问题,即每个人只负责整个流程一小块的工作,你的建议中也提到了要尽可能熟悉从业务到数据上下游的整体流程,那么这方面的实践操作是否能在大厂的流程背景下实现?还是说为了延长职业寿命,我需要可能在大厂历练几年后主动跳槽到中小厂,以换取接触完整项目的机会?
2.长沙的就业环境,我还想了个问题,就是北上广深的房价比较贵,历史的教训证明背高房贷并不明智,那如果若干年后我想着回到长沙来定居工作,那这个城市在算法等新产业方向的岗位需求又如何呢?

答二

问题一:

  1. 大厂的螺丝化情况未来应该会减弱。过去所有事情都要人做,所以企业要求员工精专某个技能。如果未来AI能解放很多领域,那对员工的核心要求也就回顺势由深度变为广度,会要求知识全面,能更好的调动AI的人
  2. 不要排斥当螺丝钉,应届生对于“接触业务”这个事情,应该是抱着”在出现接触业务的机会时尽可能抓住“的心态,而不是过分苛求在一开始就要接触业务熟悉业务。刚工作很难一开始就深入业务,可能给你的工作就是技术工作而不是业务工作。无论你一开始做什么都可以,只要在出现机会的时候积极争取即可
  3. “AI应用工程师”是个接触业务的好方向。这个职位放出来意味着企业以及想清楚了要通过AI重构某个业务,只是缺人而已,碰到这种机会要大胆点
  4. 现在不必考虑延长职业寿命的事情,把握好当下即可。事物发展太快了,过早考虑没用。比如说,以前程序员延长职业寿命的办法是降低代码质量让别人维护不了自己的代码,现在AI一出来这招完全没用了,这些人反而是最先被淘汰的
  5. 不必过分苛求去大厂。能去当然最好,但你在求职时要关注其他机会。原因:a.中国IT发展这么多年,大厂光环已经不如几年前好使了 b.最近几年大厂求职竞争尤为激烈 c.互联网大厂大部分都有AI思维,很多产品就是AI原生的。最需要AI重构业务的不是互联网大厂,而是传统企业

问题二:

  1. 长沙的互联网行业非常弱,想回长沙,你主要要瞄准传统行业以及金融行业。这些行业面对ai自身危机感也很强,也很需要人才利用AI对传统业务进行重构
  2. 长沙的核心优势就是房价低,除了房价低一无是处。职业天花板低、竞争激烈、企业普遍不遵守劳动法

大数据领域面试题(数据中台方向)

本文档整理了大数据应用开发岗位的核心面试题,涵盖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:

  1. 继承Thread类
  2. 实现Runnable接口
  3. 实现Callable接口(有返回值)
  4. 使用线程池(推荐)

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
CountDownLatch latch = new CountDownLatch(3);
// 3个线程完成后,主线程继续
latch.await();

// CyclicBarrier
CyclicBarrier barrier = new CyclicBarrier(3);
// 3个线程都到达后,一起继续
barrier.await();

// Semaphore
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); // false
System.out.println(s2 == s3); // true

使用场景:

  • 大量重复字符串的场景
  • 需要字符串比较的场景

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); // false,引用不同
System.out.println(s1.equals(s2)); // true,内容相同

String s3 = "abc";
String s4 = "abc";
System.out.println(s3 == s4); // true,字符串常量池

困难问题

Q1: String的常量池机制?

A:

字符串常量池:

  • JVM中专门存储字符串常量的区域
  • 位于方法区(JDK1.7后移到堆)
  • 可以复用字符串,节省内存

字符串创建方式:

  1. 字面量:String s = "abc",直接使用常量池
  2. **new String()**:String s = new String("abc"),创建新对象
  3. **intern()**:将字符串添加到常量池

内存优化:

  • 相同字面量共享同一个对象
  • 使用intern()可以复用字符串
  • 但需要注意性能开销

Q2: 如何正确重写equals()和hashCode()?

A:

重写equals()的步骤:

  1. 检查引用是否相同
  2. 检查对象是否为null
  3. 检查类型是否相同
  4. 比较关键字段

重写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:

执行顺序:

  1. 执行try块
  2. 如果发生异常,执行catch块
  3. 无论是否异常,都执行finally块
  4. 如果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; // 最终返回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:

实践原则:

  1. 具体异常:捕获具体异常,避免捕获Exception
  2. 不要忽略异常:至少记录日志
  3. 资源管理:使用try-with-resources自动关闭资源
  4. 异常转换:将底层异常转换为业务异常
  5. 避免空的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) {
// 空的catch块
}

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);
}
});

// Lambda方式
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提高性能
  • 需要注意线程安全

使用场景:

  • 数据量大
  • 操作独立,无状态
  • 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:日期间隔

优势:

  • 不可变,线程安全
  • API设计更清晰
  • 支持时区处理

示例:

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:

  1. 使用jmap生成堆转储文件:jmap -dump:format=b,file=heap.bin <pid>
  2. 使用jstat查看GC情况:jstat -gcutil <pid> 1000
  3. 使用MAT或VisualVM分析堆转储文件
  4. 检查是否有内存泄漏(对象无法被GC回收)
  5. 调整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对象的方式:

  1. Class.forName("类名")
  2. 对象.getClass()
  3. 类名.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:

处理方式:

  1. 编译时处理:APT(Annotation Processing Tool)
  2. 运行时处理:通过反射读取注解
  3. 字节码处理:在字节码层面处理注解

示例:

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倍)
  • 原因:方法查找、参数检查、安全检查等

优化方法:

  1. 缓存Class对象:避免重复获取
  2. 缓存Method/Field:避免重复查找
  3. 使用MethodHandle:JDK7+,性能更好
  4. 避免频繁反射:在初始化时反射,运行时直接调用

示例:

1
2
3
4
5
6
7
8
9
// 缓存Method
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
// JDK动态代理
InvocationHandler handler = (proxy, method, args) -> {
// 前置处理
Object result = method.invoke(target, args);
// 后置处理
return result;
};
Object proxy = Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
handler
);

// CGLIB动态代理
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-with-resources
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
// NIO读取文件
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:

优化方法:

  1. 使用缓冲流:BufferedInputStream/BufferedOutputStream
  2. 分块读取:不要一次性读取整个文件
  3. 使用NIO:FileChannel + MappedByteBuffer(内存映射)
  4. 多线程处理:分块并行处理

示例:

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);
}
}

// 使用NIO内存映射
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); // 8

优势:

  • 算法可以自由切换
  • 避免多重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:

  1. 加载配置文件,创建SqlSessionFactory
  2. 创建SqlSession
  3. 通过SqlSession获取Mapper代理对象
  4. 执行SQL(解析SQL、设置参数、执行、结果映射)
  5. 提交事务,关闭SqlSession

Q2: #{}和${}的区别?

A:

  • #{}:预编译,参数作为字符串处理,防止SQL注入
  • ${}:字符串替换,直接拼接SQL,存在SQL注入风险
  • #{}适用于参数值,${}适用于表名、列名等动态部分

困难问题

Q1: MyBatis的插件机制?

A:

  • 基于拦截器(Interceptor)实现
  • 可以拦截Executor、StatementHandler、ParameterHandler、ResultSetHandler
  • 实现Interceptor接口,使用@Intercepts注解指定拦截的方法
  • 常见应用:分页插件、性能监控插件

Q2: MyBatis如何实现延迟加载?

A:

  • 通过动态代理实现
  • 配置lazyLoadingEnabled=true开启延迟加载
  • 关联对象只有在真正访问时才会加载
  • 可以设置aggressiveLazyLoading控制加载行为

二、大数据技术栈

基础问题

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:

  1. Checkpoint机制:定期创建全局一致性快照
  2. 两阶段提交:配合支持事务的外部系统(如Kafka)
  3. 状态后端:RocksDB支持大状态持久化
  4. 端到端一致性:Source和Sink都支持事务

Q2: Flink如何处理数据倾斜?

A:

  1. KeyBy前加随机前缀:打散热点key
  2. 使用LocalKeyBy:在数据量大的key上先做本地聚合
  3. 调整并行度:增加下游算子并行度
  4. 使用Rebalance:强制数据重新分布
  5. 自定义分区器:根据业务特点自定义分区策略

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:

  1. 分区列表:RDD由多个分区组成
  2. 计算函数:每个分区都有计算函数
  3. 依赖关系:RDD之间存在依赖关系
  4. 分区器:可选,用于shuffle
  5. 优先位置:可选,用于数据本地性

Q3: Spark的宽依赖和窄依赖?

A:

  • 窄依赖:父RDD的每个分区最多被一个子RDD分区使用(map、filter)
  • 宽依赖:父RDD的每个分区被多个子RDD分区使用(groupBy、join)
  • 宽依赖会触发shuffle,是Stage划分的依据

一般问题

Q1: Spark的Shuffle过程?

A:

  1. Map端:
    • 数据写入环形缓冲区(默认100MB)
    • 达到阈值后spill到磁盘,按key排序
    • 合并多个spill文件
    • 生成索引文件,记录每个分区的数据位置
  2. Reduce端:
    • 通过网络拉取Map端的数据
    • 合并排序后交给reduce处理

Q2: Spark Shuffle优化?

A:

  1. 调整shuffle分区数:spark.sql.shuffle.partitions
  2. 使用Kryo序列化:减少序列化开销
  3. 启用map端聚合:spark.sql.mapSideJoin
  4. 调整缓冲区大小:spark.shuffle.file.buffer
  5. 使用Sort Shuffle:默认算法,性能更好

Q3: Spark SQL优化?

A:

  1. 使用列式存储:Parquet、ORC格式
  2. 分区裁剪:只读取需要的分区
  3. 谓词下推:在数据源层面过滤数据
  4. 列裁剪:只读取需要的列
  5. 广播Join:小表广播到所有节点
  6. 调整并行度:根据数据量设置合理的分区数

困难问题

Q1: Spark的内存管理?

A:

  • 堆内存划分:
    • Storage Memory:缓存RDD和广播变量
    • Execution Memory:shuffle、join等操作
    • User Memory:用户代码和数据结构
    • Reserved Memory:系统保留
  • 动态占用:Execution和Storage可以互相借用
  • 溢出机制:内存不足时spill到磁盘

Q2: 如何通过Spark UI定位性能瓶颈?

A:

  1. 查看Stage执行时间:找出耗时最长的Stage
  2. 查看Task分布:检查是否有数据倾斜(某些Task执行时间过长)
  3. 查看Shuffle读写:检查Shuffle数据量是否过大
  4. 查看GC时间:检查是否有GC问题
  5. 查看数据倾斜:通过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:

  1. Producer端:
    • 设置acks=all(或-1),等待所有ISR副本确认
    • 设置retries重试机制
    • 使用同步发送或回调确认
  2. Broker端:
    • 设置replication.factor>=2,多副本
    • 设置min.insync.replicas>=2,保证ISR中有足够副本
    • 设置unclean.leader.election.enable=false,禁止非ISR副本成为Leader
  3. 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数量变化
  • 过程:
    1. 所有Consumer停止消费
    2. 重新分配Partition
    3. 恢复消费
  • 问题: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:

  • 写流程:
    1. Client向NameNode请求写入文件
    2. NameNode返回DataNode列表
    3. Client将数据写入第一个DataNode
    4. DataNode之间流水线复制数据
    5. 所有副本写入成功后返回确认
  • 读流程:
    1. Client向NameNode请求读取文件
    2. NameNode返回DataNode列表和Block位置
    3. 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、内存等资源

工作流程:

  1. Client提交应用
  2. RM分配Container启动AM
  3. AM向RM申请资源
  4. RM分配Container给AM
  5. AM与NM通信启动Task
  6. Task运行并汇报状态
  7. 应用完成后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:

  1. Parser:将HQL转换为AST(抽象语法树)
  2. Semantic Analyzer:语义分析,转换为查询块
  3. Logic Plan Generator:生成逻辑执行计划
  4. Logic Optimizer:逻辑优化(谓词下推、分区裁剪等)
  5. Physical Plan Generator:生成物理执行计划(MapReduce Jobs)
  6. Physical Optimizer:物理优化(选择Join策略等)

Q2: Hive的数据倾斜问题?

A:

  • 原因:key分布不均匀、业务数据特性、建表考虑不周
  • 解决方案:
    1. 参数调节:hive.map.aggr=true、hive.groupby.skewindata=true
    2. MapJoin:小表join大表使用MapJoin
    3. 空值处理:给空值赋予随机key
    4. 不同数据类型关联:统一数据类型
    5. 特殊情况单独处理:倾斜数据单独处理再union

一般问题

Q1: Hive的优化手段?

A:

  1. 合理使用分区和分桶
  2. 使用列式存储:ORC、Parquet格式
  3. Map端聚合:hive.map.aggr=true
  4. 小文件合并:减少小文件数量
  5. 合理设置Map和Reduce数量
  6. 使用压缩:减少I/O开销
  7. 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:

写入过程:

  1. 数据写入MemTable(内存,有序结构)
  2. 写入WAL(Write-Ahead Log)保证持久化
  3. MemTable写满后,转换为Immutable MemTable
  4. 后台线程将Immutable MemTable刷盘为SSTable(Level 0)
  5. 后台线程定期合并Level i的SSTable到Level i+1

写入优势:

  • 顺序写入:MemTable刷盘是顺序写入,性能高
  • 无随机写:避免B+树的随机写放大问题
  • 高吞吐:适合写密集型场景

Q3: LSM Tree的读取流程?

A:

读取过程:

  1. 先查询MemTable(最新数据)
  2. 如果未找到,查询Immutable MemTable
  3. 如果仍未找到,从Level 0开始逐层查询SSTable
  4. 使用Bloom Filter快速过滤不存在的key
  5. 合并多个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:校验文件,校验文件完整性

索引查找过程:

  1. 通过primary.idx定位到可能包含数据的索引粒度
  2. 通过.mrk2文件找到对应的数据块在.bin文件中的位置
  3. 读取.bin文件中的数据块
  4. 解压数据块,查找具体数据

一般问题

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:分片键(可选),用于数据分片

工作原理:

  1. 写入时:根据sharding_key将数据分发到对应的分片
  2. 查询时:自动分发查询到各个分片,然后聚合结果
  3. 支持本地优先:可以设置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;

-- 与分布式表查询效果相同,但不需要创建Distributed表
-- 等价于:
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函数、取模等方式

分片策略:

  1. 随机分片:

    • 不指定sharding_key
    • 数据随机分发到各个分片
    • 适合数据均匀分布的场景
  2. Hash分片:

    • 使用sharding_key = hash(id)
    • 相同key的数据写入同一分片
    • 适合需要按key聚合的场景
  3. 取模分片:

    • 使用sharding_key = id % shard_count
    • 简单直接,但扩展性差

分片选择原则:

  • 数据均匀分布:避免数据倾斜
  • 查询本地化:尽量让查询在本地完成
  • 扩展性:支持动态添加分片

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 使用hash分片
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

合并过程:

  1. 选择多个小的分区目录
  2. 按主键排序合并数据
  3. 生成新的分区目录,Level+1
  4. 删除旧的分区目录

优化:

  • 控制合并频率,避免影响写入
  • 监控合并进度
  • 合理设置分区策略,减少合并压力
  • 使用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
  • 优先查询本地副本,减少网络开销
  • 适合副本查询场景

查询优化技巧:

  1. 使用本地表:

    • 如果知道数据在哪个分片,直接查询本地表
    • 避免分布式表的查询开销
  2. 使用SELECT … ON CLUSTER:

    • 不需要创建Distributed表,直接查询本地表
    • 查询会自动分发到所有节点并聚合结果
    • 适合临时查询或不需要长期维护分布式表的场景
  3. 合理使用分片键:

    • 查询条件包含分片键,可以只查询对应分片
    • 减少查询范围
  4. 避免跨分片JOIN:

    • 尽量在同一个分片内JOIN
    • 跨分片JOIN性能较差
  5. 使用物化视图:

    • 在本地表上创建物化视图
    • 减少跨分片查询

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-- 方式1:查询分布式表(需要先创建Distributed表)
SELECT * FROM distributed_table WHERE id = 1;

-- 方式2:使用SELECT ... ON CLUSTER(不需要创建Distributed表)
SELECT * FROM local_table ON CLUSTER 'my_cluster' WHERE id = 1;

-- 方式3:查询本地表(只查询当前节点)
SELECT * FROM local_table WHERE id = 1;

-- 设置本地优先
SET prefer_localhost_replica = 1;
SELECT * FROM distributed_table WHERE id = 1;

-- SELECT ... ON CLUSTER聚合查询
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:

  1. 查询解析:解析查询语句
  2. 路由:根据routing确定分片
  3. 分片查询:在各个分片上执行查询
  4. 结果合并:合并各分片结果
  5. 排序打分:计算相关度分数
  6. 返回结果

Q2: Elasticsearch的写入流程?

A:

  1. 写入内存缓冲区(IndexWriter Buffer)
  2. 定期refresh:将缓冲区数据写入新segment,打开segment使其可搜索
  3. 写入translog:保证数据不丢失
  4. 定期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. 触发机制:

    • 默认每1秒自动执行一次refresh
    • 可通过index.refresh_interval配置(默认1s,可设置为-1禁用自动refresh)
    • 可通过API手动触发:POST /index/_refresh
  2. 执行过程:

    • 将内存缓冲区(IndexWriter Buffer)中的数据写入新的segment文件
    • 新segment写入文件系统缓存(Page Cache),但不执行fsync
    • 打开新segment,使其可以被搜索
    • 清空内存缓冲区,准备接收新的数据
  3. 与Flush的区别:

    • Refresh:数据写入Page Cache,不刷盘,速度快,数据可能丢失
    • Flush:数据刷盘(fsync),数据持久化,速度慢,但数据安全

Refresh的表现:

  1. 近实时搜索:

    • 数据写入后,默认最多1秒后可以被搜索到
    • 这是”近实时”而非”实时”的原因
    • 可以通过手动refresh实现立即搜索:POST /index/_refresh
  2. 性能影响:

    • Refresh会创建新的segment,频繁refresh会产生大量小segment
    • 小segment过多会影响查询性能(需要查询多个segment)
    • 频繁refresh会增加CPU和I/O开销
  3. 优化策略:

    • 写入场景:可以增大refresh间隔(如30s),减少refresh频率,提高写入性能
    • 搜索场景:可以减小refresh间隔(如100ms),提高搜索实时性
    • 批量导入:可以临时禁用refresh(index.refresh_interval: -1),导入完成后恢复
  4. 实际表现:

    • 写入后立即查询可能查不到(数据还在内存缓冲区)
    • 等待1秒后可以查询到(refresh后)
    • 手动refresh后立即可以查询到
    • 数据在Page Cache中,如果服务器宕机可能丢失(需要translog恢复)

示例配置:

1
2
3
4
5
6
PUT /my_index/_settings
{
"index": {
"refresh_interval": "30s" // 30秒refresh一次,适合写入密集型场景
}
}

困难问题

Q1: Elasticsearch的分布式原理?

A:

  • 使用分片(Shard)实现水平扩展
  • 主分片负责写入,副本分片负责读取
  • 使用一致性哈希分配文档到分片
  • 支持动态调整分片和副本数量

Q2: Elasticsearch的性能优化?

A:

  • 索引优化:合理设置分片数和副本数
  • 查询优化:使用filter代替query、避免深度分页
  • 写入优化:批量写入、调整refresh间隔
  • 硬件优化:SSD、足够内存、JVM调优

Q6: Lucene的倒排合并算法?

A:

合并场景:

  • 多个Term的Posting List需要合并(AND查询)
  • 多个Term的Posting List需要取并集(OR查询)

合并算法(AND查询):

  1. 在termA的Posting List开始遍历,得到第一个元素docId=1
  2. Set currentDocId=1
  3. 在termB的Posting List中search(currentDocId),返回大于等于currentDocId的docId
  4. 如果返回的docId等于currentDocId,说明两个Term都包含该文档,加入结果
  5. 如果返回的docId大于currentDocId,更新currentDocId,继续查找
  6. 重复步骤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": "中级"}
]
}

// 使用普通对象数组,以下查询会匹配(错误)
// 查询:name=java AND value=中级
// 结果:会匹配到,因为扁平化后变成了:
// tags.name: [java, python]
// tags.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. 一对多关系:

    • 订单和订单项
    • 用户和标签
    • 文章和评论
  2. 需要精确匹配对象数组中的对象:

    • 查询特定标签组合
    • 查询特定属性组合
  3. 需要聚合嵌套对象:

    • 统计标签分布
    • 分析嵌套对象的属性

示例场景:

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}
]
}

// 查询:购买手机且价格>4000的订单
{
"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": "初级"}
]
}

// 查询:技能标签中Java为高级的用户
{
"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:

性能特点:

  • 存储开销:每个嵌套对象作为独立文档存储,增加存储空间
  • 查询性能:需要查询多个嵌套文档,性能略低于普通查询
  • 索引性能:需要为每个嵌套对象创建文档,索引速度较慢

优化策略:

  1. 合理使用:

    • 只在需要精确匹配时使用Nested
    • 嵌套对象数量不宜过多(建议<100个)
  2. 查询优化:

    • 使用inner_hits获取匹配的嵌套对象
    • 避免深度嵌套查询
  3. 替代方案:

    • 如果不需要精确匹配,使用普通对象数组
    • 考虑使用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的平衡:

  1. 问题:

    • Refresh太频繁 → 产生大量小segment → 查询性能下降
    • Refresh太慢 → 搜索延迟高 → 用户体验差
    • 小segment多 → Merge压力大 → 影响写入性能
  2. 优化策略:

    • 写入密集型:增大refresh间隔(30s-60s),减少segment产生
    • 搜索密集型:减小refresh间隔(100ms-1s),提高实时性
    • 混合场景:使用默认1s,根据实际监控调整
    • 批量导入:禁用refresh,导入完成后手动refresh一次
  3. 监控指标:

    • indices.segments.count:segment数量
    • indices.segments.memory_in_bytes:segment内存占用
    • indices.refresh.total:refresh总次数
    • indices.merges.total:merge总次数
  4. 最佳实践:

    • 监控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
    • 按Segment大小分层
    • 每层大小限制

优化:

  • 控制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:禁止读写

执行阶段:

  1. Prepare阶段:

    • 创建临时frm文件
    • 持有EXCLUSIVE-MDL锁(短暂)
    • 确定执行方式(copy/rebuild/no-rebuild)
    • 更新数据字典
    • 分配row_log对象(rebuild类型)
  2. DDL执行阶段:

    • 降级MDL锁,允许读写
    • 扫描原表数据,构造新索引
    • 记录DDL期间的增量操作(row_log)
    • 重放row_log到新表
  3. 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):

原理:

  1. 创建新表(带新结构)
  2. 创建触发器(INSERT/UPDATE/DELETE)
  3. 分批拷贝数据到新表
  4. 重命名表完成切换

优点:

  • 全程不锁表(除最后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类型操作时使用

工作原理:

  1. 记录增量:DDL执行期间,所有DML操作记录到row_log
  2. 应用增量:DDL完成后,将row_log中的操作应用到新表
  3. 保证一致性:确保DDL前后的数据一致

实现细节:

  • row_log是一个循环缓冲区
  • 记录INSERT、UPDATE、DELETE操作
  • 在Commit阶段重放最后一部分增量
  • 保证数据完整性

Q4: Online DDL的性能优化?

A:

优化策略:

  1. 选择合适的算法:

    • 优先使用ALGORITHM=INPLACE
    • 避免ALGORITHM=COPY(会锁表)
  2. 控制锁级别:

    • 使用LOCK=NONE允许并发DML
    • 避免LOCK=EXCLUSIVE(完全锁表)
  3. 分批操作:

    • 大表操作考虑分批执行
    • 使用pt-osc或gh-ost控制速度
  4. 业务低峰期:

    • 在业务低峰期执行DDL
    • 避免影响正常业务
  5. 监控和调整:

    • 监控DDL执行进度
    • 使用gh-ost可以动态调整参数
  6. 主从延迟处理:

    • 考虑先在从库执行,再切换
    • 使用pt-osc控制延迟时间

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 添加索引(INPLACE,不锁表)
ALTER TABLE table_name
ADD INDEX idx_name (column_name),
ALGORITHM=INPLACE, LOCK=NONE;

-- 添加列(INPLACE但需要rebuild)
ALTER TABLE table_name
ADD COLUMN new_col INT,
ALGORITHM=INPLACE, LOCK=NONE;

-- 修改列类型(必须COPY)
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;
// 通过userId判断相等
}

// 值对象
public class Money {
private BigDecimal amount;
private String currency;
// 通过amount和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; // 聚合根ID
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分层架构:

  1. 用户接口层(User Interface Layer):

    • 处理用户交互
    • 展示数据
    • 接收用户输入
    • 示例:Controller、DTO
  2. 应用层(Application Layer):

    • 协调领域对象
    • 处理用例
    • 事务管理
    • 示例:Application Service、Command/Query
  3. 领域层(Domain Layer):

    • 核心业务逻辑
    • 实体、值对象、聚合
    • 领域服务、领域事件
    • 示例:Entity、Value Object、Domain Service
  4. 基础设施层(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:

事件溯源定义:

  • 不存储当前状态,存储事件流
  • 通过重放事件重建状态
  • 事件是不可变的

工作原理:

  1. 业务操作产生事件
  2. 事件存储到事件存储(Event Store)
  3. 通过重放事件重建聚合状态
  4. 可以查询历史任意时间点的状态

优势:

  • 完整历史:保留所有历史记录
  • 审计:天然支持审计
  • 时间旅行:可以查询历史状态
  • 调试:可以重放事件调试问题

挑战:

  • 事件版本:事件结构可能变化
  • 性能:重建状态需要重放事件
  • 快照:需要定期创建快照优化性能

示例:

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帮助识别微服务边界

设计原则:

  1. 限界上下文即服务边界:

    • 一个限界上下文对应一个微服务
    • 避免跨服务的领域模型共享
  2. 通过API通信:

    • 服务间通过API通信
    • 使用防腐层隔离外部服务
  3. 领域事件集成:

    • 使用领域事件实现服务间集成
    • 实现最终一致性
  4. 独立数据存储:

    • 每个服务有自己的数据库
    • 避免共享数据库

实践建议:

  • 识别限界上下文:通过业务分析识别
  • 定义服务边界:限界上下文就是服务边界
  • 事件驱动:使用领域事件实现服务集成
  • 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: 请介绍一个你参与的大数据项目?

回答要点:

  1. 项目背景:业务需求、数据规模
  2. 技术选型:使用的技术栈
  3. 核心功能:主要实现的功能
  4. 个人贡献:你在项目中的角色和贡献

一般问题

Q1: 请介绍一个你主导或深度参与的大数据平台项目?

回答要点:

  1. 项目背景:业务需求、数据规模、技术挑战
  2. 技术选型:为什么选择这些技术栈
  3. 架构设计:整体架构、模块划分、数据流
  4. 核心功能:数据采集、存储、计算、服务化
  5. 遇到的挑战:
    • 数据倾斜问题及解决方案
    • 性能优化(查询优化、资源调优)
    • 稳定性保障(监控、告警、故障恢复)
  6. 项目成果:性能提升、成本降低、业务价值

Q2: 如何保障大数据任务的稳定性?

A:

  1. 任务监控:实时监控任务状态、资源使用
  2. 基线设置:设置任务完成时间基线
  3. SLA保障:定义服务等级协议
  4. 故障恢复:
    • 自动重试机制
    • 数据补偿机制
    • 降级策略
  5. 告警机制:及时发现问题

困难问题

Q1: 如何保障数据处理的准确性和时效性?

A:

  • 准确性:
    • 数据校验规则
    • 数据质量监控
    • 对账机制
    • 异常数据告警
  • 时效性:
    • 实时计算(Flink)
    • 增量处理
    • 任务优先级调度
    • 资源保障

Q2: 大数据平台的监控体系设计?

A:

  • 指标监控:任务执行时间、资源使用率、数据量
  • 日志监控:错误日志、异常日志
  • 告警机制:阈值告警、趋势告警
  • 可视化:Dashboard、报表
  • 工具:Prometheus、Grafana、ELK

6.2 性能优化

基础问题

Q1: 什么是数据倾斜?

A:

  • 数据分布不均匀,某些key的数据量远大于其他key
  • 导致某些Task处理时间过长,影响整体性能
  • 常见场景:group by、join、distinct

一般问题

Q1: 如何定位和解决数据倾斜问题?

A:

  1. 定位:
    • 通过监控发现某些Task执行时间过长
    • 查看数据分布,找出热点key
  2. 解决:
    • Flink:加随机前缀、LocalKeyBy、Rebalance
    • Spark:加随机前缀、自定义分区器、增加并行度
    • Hive:MapJoin、空值处理、倾斜数据单独处理
    • 业务层面:打散热点key、预聚合

Q2: 如何进行性能调优?

A:

  1. 资源调优:
    • CPU、内存、网络、磁盘
    • 合理设置并行度
    • 调整JVM参数
  2. 算法优化:
    • 使用更高效的算法
    • 减少Shuffle
    • 使用广播变量
  3. 存储优化:
    • 使用列式存储
    • 数据压缩
    • 分区和分桶
  4. 查询优化:
    • SQL优化
    • 索引优化
    • 物化视图

困难问题

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:

  1. CPU:top、htop、vmstat、pidstat
  2. 内存:free、vmstat、jmap
  3. 磁盘I/O:iostat、iotop
  4. 网络:netstat、ss、iftop、tcpdump
  5. 日志分析: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,结合检索和生成的技术
  • 原理:
    1. 将知识库文档向量化存储到向量数据库
    2. 用户查询时,先检索相关文档
    3. 将检索到的文档作为上下文,与大模型一起生成答案
  • 优势:
    • 减少模型幻觉
    • 支持知识更新(无需重新训练模型)
    • 可追溯答案来源
  • 应用场景:智能问答、文档检索、知识库查询

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

典型应用场景:

  1. RAG应用:

    1
    2
    3
    4
    // 文档加载 -> 向量化 -> 存储 -> 检索 -> 生成
    DocumentLoader loader = new FileSystemDocumentLoader();
    EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel();
    EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
  2. Agent应用:

    1
    2
    3
    4
    5
    // Agent可以调用工具,实现复杂任务
    Agent agent = Agent.builder()
    .tools(calculator, databaseTool)
    .chatLanguageModel(chatModel)
    .build();
  3. 数据平台集成:

    • 自动SQL生成
    • 数据查询自然语言化
    • 数据分析助手

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:

核心功能:

  1. 工作流编排:

    • 可视化拖拽式工作流设计
    • 支持条件分支、循环、并行执行
    • 支持多种节点类型:LLM、知识库检索、代码执行、API调用等
  2. 知识库管理:

    • 支持多种文档格式(PDF、Word、Markdown等)
    • 自动文档分割和向量化
    • 支持多种向量数据库
    • 文档更新和版本管理
  3. Agent构建:

    • 工具调用:支持函数调用、API调用
    • 记忆管理:对话历史、长期记忆
    • 推理能力:支持ReAct、Plan-and-Execute等模式
  4. 模型管理:

    • 支持多种模型提供商(OpenAI、Anthropic、本地模型等)
    • 模型切换和A/B测试
    • 成本控制和监控

典型应用场景:

  1. 智能数据分析助手:

    • 用户用自然语言提问
    • Agent理解问题,生成SQL查询
    • 执行查询,分析结果
    • 用自然语言返回分析结果
  2. 文档问答系统:

    • 上传企业内部文档
    • 构建知识库
    • 用户提问,RAG检索相关文档
    • 生成答案并标注来源
  3. 数据平台集成:

    • 连接数据平台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: 最近关注或学习什么新技术?

回答要点:

  1. 说明学习的新技术及其背景
  2. 学习方法和过程
  3. 实际应用场景或实践
  4. 学习收获和思考

一般问题

Q1: 如何与产品、前端、测试、运维协作?

回答要点:

  1. 需求理解:与产品充分沟通,理解业务需求
  2. 接口设计:与前端协商接口规范
  3. 测试配合:提供测试数据、环境支持
  4. 运维协作:提供部署文档、监控指标
  5. 问题处理:及时响应、快速定位、有效沟通

困难问题

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 orders
WHERE order_date >= DATE_SUB(CURRENT_DATE, INTERVAL 30 DAY)
GROUP BY user_id
ORDER BY total_amount DESC
LIMIT 10;

十、算法题考核

10.1 Easy - 数据流中的Top K元素

题目描述:
设计一个数据结构,能够实时统计数据流中出现频率最高的K个元素。

示例:

1
2
输入: [1, 1, 1, 2, 2, 3], K = 2
输出: [1, 2]

思路:

  1. 使用HashMap统计每个元素的频率
  2. 使用最小堆(大小为K)维护Top K元素
  3. 当新元素到来时,更新频率,调整堆

代码实现:

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均匀分配到节点,并支持节点动态变化

思路:

  1. 使用一致性哈希环,将节点和key都映射到环上
  2. 每个key顺时针找到第一个节点
  3. 使用虚拟节点解决负载不均衡问题
  4. 节点变化时,只影响相邻节点的数据

代码实现:

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) {
// 使用MD5或FNV哈希算法
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<>());
}
}

// 这里简化处理,实际需要遍历所有key
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. 使用两个堆:最大堆存储较小的一半,最小堆存储较大的一半
  2. 保证两个堆的大小差不超过1
  3. 最大堆的堆顶 <= 最小堆的堆顶
  4. 中位数 = 最大堆堆顶(奇数)或两个堆顶的平均值(偶数)

代码实现:

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());

// 保证两个堆的大小差不超过1
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个数据

十一、总结

大数据应用开发岗位需要掌握:

  1. 扎实的Java基础:集合、并发、JVM
  2. 大数据技术栈:Flink、Spark、Kafka、Hive、Hadoop
  3. OLAP数据库:Doris、ClickHouse
  4. 数据仓库理论:分层架构、维度建模
  5. 中间件:Redis、MySQL、消息队列
  6. 项目经验:实际项目经验、问题解决能力
  7. 工程能力:Linux、Docker、Git
  8. 学习能力:持续学习新技术
  9. 算法能力:数据结构、算法设计、大数据场景应用

面试时要注意:

  • 结合项目经验回答问题
  • 展示问题解决思路
  • 体现技术深度和广度
  • 展现学习能力和沟通能力
  • 算法题要结合大数据场景思考

症状

nextcloud macos的客户端原本都是正常使用的,升级完后连不上了。表现如下:
alt text
alt text

相关讨论

如何在 Windows 桌面客户端 3.17 中禁用证书检查?
连接失败 - 无法连接到安全服务器
Why the Nextcloud Client Does Not Accept Unsafe Connections

原因

从 3.17 版本开始,桌面客户端强制执行 HSTS header。这个header会阻止用户接受不安全的证书。因此自签名证书是无法使用的

解决方法(二选一)

1. 安装有效的 TLS 证书

2. 关闭HSTS

首先在nginx配置中对nextcloud的config移除Strict-Transport-Security相关配置,重启nginx

1
2
# 如果开启HSTS,则必须使用非自签名证书
# add_header Strict-Transport-Security "max-age=15768000";

然后删除客户端缓存并重启客户端。注意这是必须步骤。

1
rm -rf ~/Library/Caches/Nextcloud

在EPYC平台的PVE环境中直通英伟达显卡到Ubuntu虚拟机,需要按照以下步骤进行配置:

一、BIOS设置

首先需要在主板BIOS中开启相关虚拟化功能:
• AMD平台:开启SVM Mode和IOMMU

• Intel平台:开启VT-d

• 确保UEFI启动模式,关闭Legacy boot或CSM

二、PVE宿主机配置

  1. 修改GRUB启动参数
    编辑/etc/default/grub文件,修改GRUB_CMDLINE_LINUX_DEFAULT参数:
    1
    2
    3
    4
    5
    # AMD平台
    GRUB_CMDLINE_LINUX_DEFAULT="quiet amd_iommu=on iommu=pt initcall_blacklist=sysfb_init pcie_acs_override=downstream"

    # Intel平台
    GRUB_CMDLINE_LINUX_DEFAULT="quiet intel_iommu=on iommu=pt initcall_blacklist=sysfb_init pcie_acs_override=downstream"

更新GRUB配置:update-grub

  1. 加载VFIO模块
    编辑/etc/modules文件,添加以下模块:

    1
    2
    3
    vfio_iommu_type1
    vfio_pci
    vfio_virqfd
  2. 屏蔽宿主机显卡驱动
    编辑/etc/modprobe.d/pve-blacklist.conf,添加黑名单:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # NVIDIA显卡
    blacklist nvidiafb
    blacklist nouveau
    blacklist nvidia

    # AMD显卡
    blacklist amdgpu
    blacklist radeon

    # Intel集显
    blacklist snd_hda_codec_hdmi
    blacklist snd_hda_intel
    blacklist snd_hda_codec
    blacklist snd_hda_core
  3. 配置vfio-pci绑定
    获取显卡设备ID:

    1
    2
    lspci -nn | grep -i "VGA"
    lspci -nn | grep -i "audio"

创建/etc/modprobe.d/vfio.conf文件:

1
2
3
options vfio-pci ids=10de:1287,10de:0e0f
softdep nouveau pre: vfio-pci
softdep nvidia pre: vfio-pci
  1. 忽略NVIDIA显卡警告
    编辑/etc/modprobe.d/kvm.conf:

    1
    options kvm ignore_msrs=1 report_ignored_msrs=0
  2. 更新内核并重启

    1
    2
    update-initramfs -u -k all
    reboot

三、验证配置

重启后验证IOMMU是否启用:

1
2
dmesg | grep -e DMAR -e IOMMU
dmesg | grep 'remapping'

检查vfio-pci是否接管显卡:

1
lspci -nnk | grep -A3 -E "10de:1287|10de:0e0f"

四、创建Ubuntu虚拟机

  1. 虚拟机配置

• 机型:选择q35

• BIOS:改为UEFI(OVMF)

• CPU类型:设为host(提升性能兼容性)

• 内存:建议8GB及以上

• 磁盘:SCSI控制器选择VirtIO SCSI

  1. 添加显卡设备

在PVE Web界面中:

  1. 选择目标虚拟机 → “硬件”

  2. 点击”添加” → “PCI设备”

  3. 选择目标显卡设备

  4. 重要:勾选”所有功能(All Functions)”和”PCI-Express”

  5. 保存设置

  6. 关闭安全启动

在虚拟机BIOS中关闭Secure Boot,避免驱动安装问题

五、Ubuntu虚拟机内安装驱动

  1. 添加NVIDIA PPA

    1
    2
    sudo add-apt-repository ppa:graphics-drivers/ppa -y
    sudo apt update
  2. 安装推荐驱动
    在Manual Driver Search中查看显卡在环境推荐的驱动版本号
    sudo apt install nvidia-driver-xxx

  3. 禁用nouveau驱动

    1
    2
    echo -e "blacklist nouveau\noptions nouveau modeset=0" | sudo tee /etc/modprobe.d/blacklist-nouveau.conf
    sudo update-initramfs -u
  4. 重启并验证

    1
    2
    sudo reboot
    nvidia-smi

六、常见问题排查

  1. 直通成功但nvidia-smi无输出

检查是否在PVE中正确添加了PCI设备,确保勾选了”所有功能”

  1. 驱动安装报错

• 确保关闭了安全启动

• 检查内核版本兼容性

• 尝试安装开源版本驱动(对于50系列显卡)

  1. 性能问题

• 确保CPU类型设置为host

• 启用NUMA支持

• 配置大页内存(可选)

通过以上步骤,您应该能够在EPYC平台的PVE环境中成功直通英伟达显卡到Ubuntu虚拟机,实现接近原生性能的GPU加速功能。