1、B 树 与 B + 树
(1)基础定义
二者都是多路平衡查找树,专为磁盘 IO 优化设计(磁盘按页加载数据,减少 IO 次数是核心目标)。
B 树
- 所有节点(根、中间、叶子)都存储索引键 + 完整数据记录;
- 叶子节点无链表串联;
- 查找一条数据可能命中任意层级节点。
B + 树(MySQL InnoDB 索引默认结构)
- 非叶子节点只存索引键,不存真实数据,全部数据仅放在叶子节点;
- 叶子节点用双向链表有序串联;
- 主键、二级索引结构区分:
- 主键索引:叶子存整行完整数据;
- 二级索引:叶子只存储主键值。
(2)核心对比
| 特性 | B 树 | B + 树 |
|---|---|---|
| 数据存放位置 | 所有节点都存数据 | 仅叶子节点存数据 |
| 叶子节点关系 | 无链表相连 | 有序双向链表 |
| 单次查找 IO 次数 | 不稳定,最优根节点命中 | 高度固定,IO 次数稳定 |
| 范围查询 | 需要多次遍历树 | 遍历链表即可,效率极高 |
| 磁盘利用率 | 较低,非叶子占用大量空间 | 非叶子节点紧凑,一页能放下更多索引,树更低矮 |
适用场景
- B 树:MongoDB 等数据库索引、文件系统索引;
- B + 树:MySQL InnoDB,擅长等值查询 + 范围查询、排序、分页。
2、HTTP 常用状态码分类与作用
状态码由三位数字组成,分为 5 大类:
1xx 信息类(协商阶段)
- 100 Continue:客户端可继续发送剩余请求体;
- 101 Switching Protocols:协议切换(HTTP 升级为 WebSocket)。
2xx 请求成功
- 200 OK:常规请求成功;
- 201 Created:资源创建成功(POST 新增数据);
- 204 No Content:执行成功无返回数据;
- 206 Partial Content:断点续传分片下载。
3xx 重定向
- 301 Moved Permanently:永久重定向,浏览器缓存跳转地址;
- 302 Found:临时重定向,每次请求都跳转;
- 304 Not Modified:协商缓存命中,资源未修改,直接用本地缓存。
4xx 客户端错误
- 400 Bad Request:请求参数格式错误;
- 401 Unauthorized:未登录、无身份认证;
- 403 Forbidden:登录成功,但权限不足;
- 404 Not Found:资源地址不存在;
- 405 Method Not Allowed:请求方式不支持(GET 接口用 POST 调用);
- 413 Payload Too Large:请求体超出服务器限制。
5xx 服务端内部错误
- 500 Internal Server Error:服务器代码异常、空指针、SQL 报错;
- 502 Bad Gateway:网关 / 反向代理后端服务挂了;
- 503 Service Unavailable:服务过载、停机维护、限流拒绝;
- 504 Gateway Timeout:网关等待下游服务超时。
3、INNER JOIN、LEFT JOIN(左连接)
数据表约定
A 表(左表)、B 表(右表),关联字段 id
-
INNER JOIN 内连接 只返回两张表关联条件完全匹配的数据; 两边不匹配的数据全部丢弃。
SELECT * FROM A INNER JOIN B ON A.id=B.id;
适用:需要两边数据都存在的关联查询。
-
LEFT JOIN 左外连接 以左表 A 为全集 ,左表所有数据全部保留; 右表匹配上则拼接数据,没匹配上时右表字段全部填充
NULL。SELECT * FROM A LEFT JOIN B ON A.id=B.id;
补充 RIGHT JOIN、FULL JOIN
- RIGHT JOIN:右表全集,左表无匹配补 NULL;
- FULL JOIN:两张表所有数据都保留,无匹配端补 NULL(MySQL 不原生支持)。
经典场景
LEFT JOIN:查询所有订单,连带可选的支付记录;没有支付的订单也要展示。
4、JVM 内存模型(运行时数据区)
JVM 规范定义的运行时五大内存分区,分为线程私有、线程共享两大类:
一、线程私有(每个线程独立,生命周期随线程)
- 程序计数器 PC Register 记录当前线程执行到哪一行字节码指令; 唯一不会发生 OOM 的区域。
- 虚拟机栈(Java 栈) 存放栈帧:局部变量表、操作数栈、动态链接、方法出口; 基本数据类型、对象引用存在此处; 栈深度过大:StackOverflowError;栈内存不足:OOM。
- 本地方法栈 执行 native 本地方法(C/C++),结构和虚拟机栈一致。
二、线程共享(全局唯一,JVM 启动创建)
- 堆 Heap(重中之重) 所有对象实例、数组分配在堆;GC 主要回收区域; 细分:新生代(Eden、S0、S1)、老年代;堆内存耗尽抛出 OOM。
- 方法区(元空间 Metaspace,JDK8 优化) JDK7 及以前:永久代; JDK8 改为本地内存的元空间:存储类结构、常量池、静态变量、注解、编译后的字节码; 元空间溢出也会 OOM。
易混区分
- JVM 内存模型 ≠ Java 内存模型(JMM,规定多线程可见性、有序性);
- JMM:主内存 + 工作内存,volatile、synchronized 依托 JMM 保证并发安全。
5、MVCC 多版本并发控制(InnoDB 核心)
核心作用
不加锁实现读不加锁、写加锁,解决读写阻塞,支撑 RC、RR 隔离级别,基于快照读实现无锁并发。
底层三大核心组件
- 隐藏字段 每行数据内置三个隐藏列:
DB_TRX_ID:最后修改该行的事务 ID;DB_ROLL_PTR:回滚指针,指向 undo log 里的历史版本;DB_ROW_ID:无主键时自动生成主键。
- undo log 回滚日志 记录数据修改前的旧版本数据,通过回滚指针串联成版本链; 事务回滚依靠 undo log,同时提供历史快照数据。
- read view 读视图 执行快照读时生成,用来判断当前版本数据对当前事务是否可见。
RR(可重复读)实现逻辑
事务第一次快照读时生成 ReadView,整个事务复用同一个视图; 只能看到视图创建前已提交的数据,期间其他事务修改不可见,实现可重复读。
两种读取模式
- 当前读(加锁读):select ... for update、update、delete,加行锁读取最新数据;
- 快照读(普通 select):走 MVCC 读取历史版本,无锁。
解决问题
解决 RC/RR 下脏读、不可重复读;InnoDB 依靠 MVCC + 间隙锁解决幻读。
6、双线程循环 count++ 100 次,最终结果分析
结论先行
- 几乎不可能等于 200;
- 理论最小值:100;最大值理想 200;中间区间随机。
前置原理:count++ 非原子操作
count++ 编译后拆解为三步独立 CPU 指令:
- load:从主内存读取 count 值到线程本地工作内存;
- add:本地内存执行 + 1 运算;
- save:将计算结果写回主内存。 三步无锁、无同步,线程之间会互相覆盖。
场景拆解
设初始 count=0,线程 A、B 各循环 100 次:
1)出现数据覆盖(典型导致结果偏小)
示例:
- A 读取 count=0,还没执行 + 1 和写回;
- B 同时读取 count=0;
- A 本地 + 1=1,写回主内存 count=1;
- B 本地 + 1=1,写回主内存 count=1; 两次自增只生效一次,计数被覆盖,少 1 次。 大量并发覆盖叠加,最终总值小于 200。
2)最小值为什么是 100?
极端最坏场景: 线程 A 完整执行全部 100 次自增写入完毕; 线程 B 每一次读取的都是 A 最终写完后的最新值,B 的 100 次修改全部被无效覆盖,最终只有 A 的 100 次有效计数,count=100。
如何保证最终一定 200
保证count++原子性:
synchronized包裹自增代码块;- 使用
AtomicInteger原子类(CAS 无锁); - ReentrantLock 加锁。
7、项目中遇到的问题 + 标准回答思路(通用面试话术)
答题固定结构:背景 → 问题现象 → 定位过程 → 解决方案 → 复盘优化
示例 1:线上接口慢,SQL 慢查询
- 背景:订单列表分页接口,数据量上万后接口响应 5s+;
- 现象:用户加载页面卡顿,监控大量慢 SQL 告警;
- 定位:开启慢查询日志,EXPLAIN 分析发现无索引、全表扫描、limit 大偏移量;
- 解决:
- 给查询条件建立联合 B + 索引;
- 大偏移分页改造为主键游标分页;
- 热点数据接入 Redis 缓存;
- 复盘:上线前强制 SQL 审核,压测验证大数据量性能。
示例 2:Redis 缓存击穿
- 问题:热点商品 key 过期瞬间大量请求击穿缓存打垮 DB;
- 方案:互斥锁、逻辑过期时间、热点 key 永不过期。
示例 3:OOM 内存溢出
- 定位:dump 堆快照,MAT 分析发现大集合未释放、线程局部变量堆积;
- 优化:手动清空大集合、合理设置 JVM 参数、优化代码引用。
答题要点:不说空话,带上监控、日志、工具(Arthas、SkyWalking、EXPLAIN、MAT)。
8、分布式锁如何保证事务提交前锁不提前释放
痛点
Redisson 等分布式锁如果在事务未提交时,锁租期到期自动释放,其他线程抢占锁会引发脏数据。
主流解决方案
方案 1:Redisson 自带看门狗(自动续期,最优)
- 加锁成功后启动后台守护线程(看门狗);
- 锁有效期默认 30s,每隔 10s 检测持有锁的业务线程是否还在运行;
- 业务未执行完毕、事务未提交,自动延长锁过期时间;
- 业务正常结束主动解锁,看门狗停止;业务宕机,锁到期自动释放,不会死锁。
注意:必须使用 Redisson 的
RLock,不要手动设置较短过期时间关闭看门狗。
方案 2:手动控制锁生命周期
- 先开分布式锁 → 再开启数据库事务;
- 事务执行完毕、commit 提交成功后,主动释放锁;
- 禁止事务未提交就解锁;
- 兜底:设置合理锁超时,超时时间必须大于业务事务最大执行耗时。
方案 3:锁粒度后置
把解锁逻辑放到事务提交之后的 finally 代码块,保证只有事务落地才释放锁。
禁忌操作
不要在事务中途主动解锁;不要锁过期时间设置比业务执行时间短。
9、AI 框架 ChatMemory 对话记忆实现原理
保存历史上下文对话,让大模型具备连续对话记忆,不用每次重复输入历史对话。
主流实现方案
1、内存型 Memory(临时,服务重启丢失)
- 以会话 ID 为 key,内存 Map 存储用户 + AI 历史消息列表;
- 每次提问前拼接全部历史消息 + 当前用户问题,一起送入 LLM;
- 缺点:进程重启丢失数据,只适合测试。
2、持久化 Memory(生产常用)
存储介质:Redis、MySQL、MongoDB
- 按
sessionId划分独立会话; - 每条消息记录角色(user/assistant/system)、时间、内容;
- 策略 1:全量上下文拼接,会话越长 Prompt 越长,token 飙升;
- 策略 2:滑动窗口截断,只保留最近 N 轮对话,控制 token 消耗;
- 策略 3:摘要压缩,定时把久远对话总结为简短摘要,减少上下文长度。
3、向量型记忆(长期记忆 RAG)
历史对话向量化存入向量库; 用户新问题向量化检索相似度最高的历史对话,注入上下文,实现超长周期记忆。
核心流程
用户提问 → 查询当前 session 历史记忆 → 组装 prompt 上下文 → 调用大模型 → 模型回复后把本轮问答存入 Memory。
10、EasyExcel 五万条数据稳定导入 + 弱网下断点续传落地方案
一、五万条稳定导入优化(EasyExcel 原生优势 + 业务优化)
1、EasyExcel 本身优势
对比 POI:POI 加载全表到内存极易 OOM;EasyExcel 基于 SAX 逐行流式读取,一行读取一行处理,内存占用极低,五万条轻松无压力。
稳定落地优化手段
- 分批读取 + 分批入库 设置每次读取 100~500 条为一批,攒批后执行批量 INSERT;避免单条循环插入数据库,降低 IO 开销;
- 异步解耦(大数据量) 前端上传文件→后端接收文件存临时存储(OSS / 本地)→返回任务 ID; MQ 发送导入任务,消费者异步读取 EasyExcel 解析入库,前端轮询任务进度;
- 前置数据校验 解析阶段校验格式、手机号、重复主键、必填项,非法数据记录行号与错误原因,导出错误 Excel,不阻塞整体导入;
- 内存防护 关闭不必要对象缓存,解析时及时回收临时对象,禁止一次性加载全表数据;
- 事务控制:分批批次内开启事务,失败回滚当前批次,已成功批次不回滚。
二、弱网环境断点续传实现
整体思路:前端分片上传 + 后端断点标记 + EasyExcel 分片解析
-
前端分片切割 前端将 Excel 按固定大小分片(5MB / 片),携带文件唯一 md5、分片序号、总分片数; 弱网断连后,前端只重新上传未完成分片; 后端记录每个文件分片上传状态,全部分片上传完成后合并为完整 Excel。
-
解析阶段断点续导(解析中途失败续跑)
- 数据库记录表:
导入任务表(taskId,已解析行数、状态、文件地址、失败标记); - EasyExcel 自定义监听器,每解析 N 行更新数据库已处理行数;
- 程序崩溃、网络中断、服务重启后,用户发起续导请求;
- 后端读取任务已解析行数,使用 EasyExcel跳过前面已处理行,从断点行继续解析入库。
- 弱网兜底 文件先持久化到云存储,不占用服务器临时内存;网络波动只重传分片,无需从头上传整个 Excel; 超时重传机制,前端指数退避重试分片上传。
整体链路
前端分片上传→后端合并完整文件→创建导入任务→EasyExcel 流式分批解析 + 定时记录断点→异常后根据 taskId 从断点续解析→导入完成生成结果报告。