# 一、专业技能部分
## 1. Java & 并发编程
### 线程池的核心参数(corePoolSize、maximumPoolSize、keepAliveTime 等)和工作原理?
**答:**
线程池的 7 个核心参数:
1. **corePoolSize**:核心线程数,即使空闲也会保留的线程数量。
2. **maximumPoolSize**:最大线程数,线程池允许创建的最大线程总数。
3. **keepAliveTime**:非核心线程空闲后的最大存活时间。
4. **unit**:keepAliveTime 的时间单位。
5. **workQueue**:阻塞队列,用于存储等待执行的任务。
6. **threadFactory**:线程工厂,用于创建新线程。
7. **handler**:拒绝策略,当队列和线程池都满时的处理策略。
**工作原理:**
1. 任务提交时,如果当前线程数 < corePoolSize,直接新建核心线程执行任务。
2. 如果线程数达到 corePoolSize,任务进入阻塞队列排队。
3. 如果队列已满,且线程数 < maximumPoolSize,创建非核心线程执行任务。
4. 如果队列满且线程数达到最大值,执行拒绝策略。
### ThreadLocal 的原理、使用场景和内存泄漏风险?
**答:**
**原理:**
每个 Thread 内部都有一个 ThreadLocalMap,以 ThreadLocal 实例为 key,存储线程私有数据。多个线程之间互不干扰,实现线程隔离。
**使用场景:**
- 存储用户登录信息、身份上下文
- 多租户、事务上下文
- 避免方法层层传递参数
**内存泄漏原因:**
- ThreadLocalMap 的 Entry 是弱引用,但 value 是强引用。
- 线程池中的线程会被复用,如果使用完不调用 remove(),value 会一直存在,造成内存泄漏。
**避免方式:**
使用完必须调用 `threadLocal.remove()` 清理数据。
### synchronized 和 ReentrantLock 的区别?
**答:**
1. **实现层面**
- synchronized:JVM 层面实现。
- ReentrantLock:JDK 代码层面实现。
2. **使用方式**
- synchronized:自动加锁、自动释放锁,异常也会释放。
- ReentrantLock:需要手动 lock()、unlock(),必须在 finally 中解锁。
3. **功能特性**
- ReentrantLock 支持**公平锁、可中断、超时锁、多 Condition**。
- synchronized 不支持。
4. **性能**
- 低竞争:两者差不多。
- 高并发:ReentrantLock 吞吐量更稳定。
### 你在项目中是如何解决并发问题的?(可以结合校园跑腿平台的订单超卖问题)
**答:**
在校园跑腿项目中,订单抢单、接单、支付结算都存在并发问题,我主要做了:
1. **Redis 分布式锁**
接单时使用分布式锁,保证同一订单同一时间只能被一个骑手接单,防止超卖。
1. **数据库乐观锁(版本号机制)**
订单状态、资金变动时使用 version 字段,更新时判断版本号是否一致,不一致则重试。
1. **接口幂等性**
防止用户/骑手重复提交、重复支付。
1. **线程池规范化使用**
异步任务统一使用自定义线程池,避免频繁创建销毁线程,提升并发稳定性。
### ConcurrentHashMap 是如何保证线程安全的?(1.8版本)
**答:**
Java 1.8 版本的 ConcurrentHashMap 抛弃了 1.7 的分段锁(Segment)设计,改为:
1. **CAS + synchronized 细粒度锁**:对每个数组节点(Node)加 synchronized 锁,只锁定当前操作的桶,而非整个数组,并发粒度更细。
2. **volatile 保证可见性**:节点的 val 和 next 字段用 volatile 修饰,确保多线程下数据可见性。
3. **无锁 CAS 操作**:初始化、扩容、插入节点时,优先用 CAS 保证原子性,失败后再降级为 synchronized 锁。
相比 1.7 的分段锁,1.8 减少了锁竞争,高并发下性能更优。
### Java 线程池的核心参数有哪些?你的项目中,线程数是怎么评估设置的?
**答:**
**核心参数**:corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler(同前文线程池参数)。
**线程数设置依据**:
线程数需根据任务类型评估:
1. **CPU 密集型任务**(如计算、排序):线程数 = CPU 核心数 + 1(减少线程切换开销,+1 为了应对偶发的页缺失等情况)。
2. **IO 密集型任务**(如数据库操作、Redis 调用、接口调用):线程数 = CPU 核心数 × 2 或更高(我的项目中设置为 CPU 核心数 × 4,因为跑腿平台大量操作数据库/Redis,线程大部分时间在等待 IO 响应)。
实际项目中还会结合压测结果调整,比如监控线程池的队列长度、活跃线程数,避免队列堆积或线程数过多导致上下文切换频繁。
## 2. 数据库(MySQL、Redis)
### MySQL 索引失效的常见场景?你在实习中如何优化复合索引?
**答:**
**索引失效常见场景:**
1. 对索引列使用函数、计算、类型转换。
2. 使用 !=、<>、is not null。
3. like '%xxx' 左边通配符。
4. 复合索引不满足最左前缀原则。
5. MySQL 优化器认为全表扫描更快。
**我在实习中的优化:**
1. 使用 explain 分析慢查询,重点看 type、key、rows、Extra。
2. 根据业务高频 SQL 重新设计**复合索引顺序**,把最常用、区分度最高的字段放左边。
3. 删除重复、冗余、从未使用的索引。
4. 避免回表,必要时使用覆盖索引。
最终核心接口响应时间明显下降。
### Redis 的数据类型和应用场景?你项目中用 Redis 做了什么?
**答:**
**常用数据类型与场景:**
- String:缓存、分布式锁、计数器、Token。
- Hash:存储对象、用户信息、骑手坐标。
- List:队列、消息列表。
- Set:去重、共同关注。
- ZSet:排行榜、延时任务。
**我在项目中的实际使用:**
1. 缓存骑手实时地理位置(Hash)。
2. 订单防重、幂等校验(String + 过期时间)。
3. Redis 分布式锁。
4. 校园智能助理对话上下文缓存。
5. 热点数据缓存,减轻 DB 压力。
### 如何解决缓存穿透、缓存击穿、缓存雪崩?
**答:**
1. **缓存穿透**:查询不存在的数据,直接打到 DB。
解决:缓存空值、布隆过滤器过滤非法 key。
1. **缓存击穿**:热点 key 过期,大量请求同时访问 DB。
解决:热点 key 永不过期、互斥锁。
1. **缓存雪崩**:大量 key 同时过期,DB 压力剧增。
解决:过期时间加随机值、多级缓存、服务降级、集群高可用。
### 事务的隔离级别和实现原理?
**答:**
**4 个隔离级别:**
1. 读未提交(Read uncommitted):可能脏读。
2. 读已提交(Read committed):解决脏读,可能不可重复读。
3. 可重复读(Repeatable read):MySQL 默认,解决不可重复读,可能幻读。
4. 串行化(Serializable):最高级别,无并发问题,性能低。
**实现原理:**
基于 **MVCC(多版本并发控制)** + 锁机制。
- 读已提交:每次 select 生成新的 ReadView。
- 可重复读:事务第一次 select 生成 ReadView,全程复用。
通过 Undo log 保留历史版本,实现无锁非阻塞读取。
### MySQL:聚簇索引和二级索引有什么区别?什么是回表?
**答:**
1. **聚簇索引(主键索引)**:
- 索引和数据存储在一起,InnoDB 中主键索引的叶子节点存储整行数据。
- 一张表只有一个聚簇索引,默认是主键,无主键则选唯一非空索引,否则自动生成隐藏列。
2. **二级索引(非主键索引)**:
- 索引和数据分离,叶子节点存储的是主键值,而非整行数据。
- 一张表可以有多个二级索引(如普通索引、复合索引)。
**回表**:
通过二级索引查询时,先找到主键值,再通过主键索引(聚簇索引)查询整行数据的过程,称为回表。回表会增加 IO 次数,影响查询效率,可通过**覆盖索引**避免(查询列全部包含在二级索引中,无需回表)。
### 什么叫“复合索引覆盖”?请讲解一下联合索引的“最左前缀匹配法则”。
**答:**
1. **复合索引覆盖(覆盖索引)**:
查询的所有列(select 字段 + where 条件字段)都包含在创建的复合索引中,MySQL 无需回表,直接从索引中获取所有需要的数据,大幅提升查询效率。
例如:创建复合索引 (user_id, order_id, amount),查询 `select amount from order where user_id=1 and order_id=100` 时,可直接从索引中获取 amount,无需回表。
1. **最左前缀匹配法则**:
MySQL 对复合索引的匹配遵循“最左优先”原则,只有从索引最左侧字段开始匹配,才能命中索引。
例如:创建复合索引 (a, b, c),以下场景:
- 条件 `where a=1` → 命中索引;
- 条件 `where a=1 and b=2` → 命中索引;
- 条件 `where a=1 and b=2 and c=3` → 命中索引;
- 条件 `where b=2` / `where b=2 and c=3` → 未命中索引;
核心:复合索引的字段顺序决定了查询条件的匹配有效性,设计时需将高频查询字段放在左侧。
### 你提到的“SQL全文索引”,MySQL 的全文索引性能相对较差,业务上为什么没有引入 ElasticSearch 或者单纯借助 Redis?
**答:**
选择 MySQL 全文索引而非 ES/Redis 的核心是**技术选型的 Trade-off(权衡)**:
1. **成本与复杂度**:
业务场景是校园项目,全文检索需求仅针对少量静态内容(如订单备注、用户反馈),数据量小(百万级以内),引入 ES 会增加集群部署、维护、学习成本,Redis 也无法高效支持复杂的全文检索语法。
1. **性能满足需求**:
对全文索引的 QPS 要求低(峰值每秒几十次),MySQL 全文索引通过合理配置(如分词规则、索引优化)完全能满足业务需求,无需“杀鸡用牛刀”。
1. **开发效率**:
直接基于 MySQL 开发,无需跨系统调用,减少接口联调、数据同步的工作量,符合校园项目快速迭代的需求。
若后续数据量或 QPS 大幅提升,可平滑迁移至 ES,现阶段优先保证开发效率和维护成本可控。
## 3. 框架(Spring、Spring Boot、MyBatis)
### Spring 的 IOC 和 AOP 原理?
**答:**
**IOC(控制反转):**
- 将对象的创建、依赖管理交给 Spring 容器。
- 通过反射创建 Bean,通过 DI 注入依赖。
- 降低耦合,方便单元测试、扩展、维护。
**AOP(面向切面编程):**
- 基于动态代理:
- 接口:JDK 动态代理。
- 类:CGLIB 代理。
- 统一处理日志、权限、事务、异常等横切逻辑,不侵入业务代码。
### Spring Boot 自动配置原理?
**答:**
1. `@EnableAutoConfiguration` 开启自动配置。
2. 读取 `META-INF/spring/...AutoConfiguration.imports` 下的配置类。
3. 通过条件注解 `@ConditionalOnClass、@ConditionalOnMissingBean` 等判断是否需要装配。
4. 自动创建 Bean 并注入容器,实现零配置、快速开发。
### MyBatis 的 #{} 和 ${} 区别?MyBatis-Plus 优势?
**答:**
**#{}:**
- 预编译,参数用 ? 占位。
- 防止 SQL 注入。
- 适合传参。
**${}:**
- 直接字符串拼接。
- 有 SQL 注入风险。
- 适合动态表名、排序字段、动态列名。
**MyBatis-Plus 优势:**
- 封装通用 CRUD,不用写 XML。
- Lambda 条件构造器,代码更优雅。
- 支持分页、逻辑删除、乐观锁、自动填充。
- 大幅提升开发效率。
### MyBatis 的流式查询底层是怎么实现的?用了哪个核心类?流式查询必须保持数据库连接不中断,你是如何管理事务/数据库连接的?
**答:**
1. **底层实现与核心类**:
MyBatis 流式查询基于 JDBC 的 `ResultSet` 游标实现,核心类是 `ResultHandler`(自定义逐行处理逻辑)和 `Cursor` 接口(迭代器式读取)。
- 常规查询:MyBatis 会一次性将 ResultSet 加载到内存,封装为 List 返回。
- 流式查询:通过 `ResultHandler` 逐行读取 ResultSet 中的数据,读取一行处理一行,不缓存全部数据,内存占用极低。
1. **数据库连接/事务管理**:
流式查询需要保持数据库连接不关闭(否则 ResultSet 失效),我通过以下方式管理:
- **手动控制事务**:在流式查询方法上声明 `@Transactional`,并设置事务传播级别为 `REQUIRES_NEW`,保证独立事务。
- **关闭自动提交**:设置 `jdbcTemplate.setFetchSize(Integer.MIN_VALUE)`,告诉 MySQL 采用流式传输模式。
- **及时释放资源**:处理完数据后,在 finally 中手动关闭 ResultSet、Statement、Connection,或通过 Spring 的事务管理器自动释放。
- **异步处理**:将流式查询放入独立线程池执行,避免占用主线程的数据库连接。
## 4. 工程化 & DevOps
### Git 工作流(GitFlow、GitHub Flow)?如何处理代码冲突?
**答:**
**GitFlow:**
- master:生产代码
- develop:开发主干
- feature:功能分支
- release:发布分支
- hotfix:紧急修复
**GitHub Flow:**
- 简单轻量,主干可部署,功能分支合并后即上线。
**我处理冲突:**
1. 先拉取最新代码。
2. 使用 IDE 对比工具,逐行解决冲突。
3. 保留正确逻辑,删除无效代码。
4. 本地编译测试通过后再提交。
### CI/CD 流水线关键环节?Shell 脚本做了什么?
**答:**
**流水线环节:**
代码提交 → 代码检查 → 单元测试 → 构建 → 镜像构建 → 推送 → 部署 → 自动化测试。
**我的 Shell 脚本功能:**
1. 停止旧容器。
2. 拉取最新镜像。
3. 启动新容器,挂载配置、日志。
4. 健康检查。
5. 完成 Docker 一键部署与热更新。
### Docker 核心概念?如何服务编排?
**答:**
**核心概念:**
- 镜像(Image):应用 + 运行环境的只读模板。
- 容器(Container):镜像运行实例,相互隔离。
- 仓库(Repository):存储、分发镜像。
**编排:**
使用 Docker Compose,通过 yaml 文件统一管理多个服务,一条命令启动/停止/重启整套环境。
## 5. AI 工具 & 前端
### 如何使用 Cursor/Copilot 提升效率?
**答:**
1. 根据注释生成标准代码。
2. 快速生成 CRUD、工具类、异常处理、单元测试。
3. 辅助排查 bug、解释复杂代码。
4. 生成 SQL、正则、脚本。
5. 代码重构、优化建议。
提高编码速度,减少重复劳动。
### 前端掌握程度?能独立做页面吗?
**答:**
掌握 HTML + CSS + JavaScript + Vue 基础。
能独立完成:
- 响应式页面布局
- 表单、弹窗、列表、路由
- 与后端接口联调
可以独立完成简单后台管理页面、移动端页面。
# 二、实习经历部分
## 1. CI/CD 自动化交付流水线
### 你负责的 CI/CD 完整流程?
**答:**
1. 开发提交代码到 Git。
2. CI 触发:代码检查、单元测试。
3. Maven 构建 jar。
4. 构建 Docker 镜像并推送到仓库。
5. 执行部署脚本,拉取镜像、停止旧容器、启动新容器。
6. 健康检查,完成发布。
### Shell 脚本实现了什么?Docker 热更新?
**答:**
脚本实现:
- 拉取最新镜像
- 安全停止旧容器
- 删除旧容器
- 启动新容器并挂载目录、端口
- 检查服务是否启动成功
实现无停机热更新,一键完成发布。
### 如何衡量减少 80% 人工部署成本?
**答:**
原来人工:
- 拉代码、打包、上传、重启、检查
- 每次 20~30 分钟,易出错。
自动化后:
- 全程 3~5 分钟,无人干预。
按时间与人力成本计算,人工成本降低约 80%。
## 2. 大数据报表导出性能治理
### 10W+ 导出 OOM 根本原因?
**答:**
传统 POI 会一次性把所有数据加载到内存,数据量一大就堆内存溢出,导致服务崩溃(具体表现为:JVM 老年代被瞬间塞满,触发 Full GC 但无法回收,最终抛出 OutOfMemoryError)。
### EasyExcel 流式写入如何解决 OOM?为什么用 EasyExcel 而不是原始的 Apache POI?
**答:**
1. **EasyExcel 解决 OOM 的核心原理**:
EasyExcel 采用 **SAX 事件驱动模式** 解析/写入 Excel,而非 POI 的 DOM 模式:
- SAX 模式:一行行读取数据、一行行写入磁盘临时文件,内存中仅保留当前处理的行数据,内存占用稳定在 MB 级,不受数据量影响。
- 配合 MyBatis 流式查询,实现“读一行写一行”,即使 100W 行数据也不会 OOM。
1. **与 Apache POI 的区别**:
- 原始 POI(DOM 模式):将整个 Excel 文档解析为内存中的对象树,数据量越大,内存占用越高,极易 OOM。
- EasyExcel:无对象树,无全量数据缓存,专门针对大文件导出做了内存优化,还封装了流式 API,开发成本更低。
### MyBatis 流式查询 ResultHandler 原理?
**答:**
- 使用 ResultHandler 逐行读取数据。
- 不把结果集一次性加载到内存。
- 一边读一边处理,内存占用极低。
从根本上解决大数据量查询内存溢出问题。
### 10万条数据导出耗时量级大概有多长?如果用户点击导出后网断了或者关闭页面怎么办?
**答:**
1. **耗时量级**:
10 万条数据导出耗时约 30~60 秒(取决于数据复杂度、服务器性能),核心耗时在数据库流式查询和文件写入,内存无压力。
1. **断网/关闭页面的解决方案**:
我采用“异步导出 + 结果通知”的方案:
- 前端点击导出后,后端生成异步任务,返回任务 ID,前端轮询/通过 WebSocket 监听任务状态。
- 导出任务在后台线程池执行,完成后将文件上传至 OSS,生成临时下载链接。
- 无论用户是否断网,导出完成后通过站内信/短信推送下载链接,用户可随时下载。
- 额外设置文件过期策略(如 24 小时后自动删除),避免 OSS 存储浪费。
## 3. SQL 深度治理
### Explain 实战:Explain 分析时,你最关注哪几个列?type 字段能达到什么级别才算合格?Extra 里面看到什么说明语句很拉胯?
**答:**
1. **核心关注列**:
- `type`:访问类型(核心指标,体现查询效率)。
- `key`:实际命中的索引(确认索引是否生效)。
- `rows`:预估扫描行数(越小越好,反映查询范围)。
- `Extra`:额外信息(反映查询的优化点)。
2. **type 字段合格标准**:
type 级别从优到差:`system > const > eq_ref > ref > range > index > ALL`。
- 合格线:至少达到 `range`(范围扫描,如 between、in 等)。
- 理想状态:核心查询达到 `ref`(非唯一索引匹配)或 `eq_ref`(唯一索引匹配)。
- 需优化:`index`(全索引扫描)、`ALL`(全表扫描)。
1. **Extra 中“拉胯”的标识**:
- `Using filesort`:MySQL 需额外排序(如 order by 字段无索引),性能差。
- `Using temporary`:创建临时表(如 group by 无索引),内存/IO 开销大。
- `Using join buffer`:连接查询未命中索引,使用连接缓冲区,需优化关联字段索引。
### 如何设计复合索引顺序?
**答:**
1. 最左前缀原则。
2. 等值查询放左边。
3. 区分度高的放左边。
4. 范围条件放右边。
结合业务高频 SQL 设计,确保大部分查询能命中索引。
### 如何衡量核心接口耗时降低 40%?
**答:**
优化前:接口平均响应时间约 1000ms,QPS 约 50(峰值)。
优化后:接口平均耗时约 600ms,QPS 提升至 80+。
通过压测工具(JMeter)对比优化前后的响应时间、QPS、错误率,结合生产环境的监控数据(Prometheus + Grafana),确认核心接口耗时下降 40%,且系统稳定性提升(Full GC 次数减少)。
# 三、项目经历部分
## 项目一:基于地理位置信息的校园智能跑腿平台
### 1. 单账号双身份架构
#### ThreadLocal 身份上下文?如何防止污染?
**答:**
登录后 JWT 解析出用户/骑手身份,存入 ThreadLocal。
接口中直接获取身份,实现权限控制。
**必须在请求结束后调用 remove()**,否则线程池复用会导致身份错乱。
#### JWT + 拦截器流程?Token 过期?JWT一旦签发,服务端无法主动让它过期。如果用户修改了密码,怎么让之前的JWT立刻失效?
**答:**
1. **JWT + 拦截器基础流程**:
- 前端请求带 Token → 拦截器解析 Token → 验证签名/有效期 → 提取用户信息存入 ThreadLocal → 放行;验证失败返回 401。
- Token 过期:前端跳转登录页,或实现“刷新 Token”机制(签发短期 accessToken + 长期 refreshToken,accessToken 过期后用 refreshToken 续期)。
2. **JWT 主动失效方案(密码修改场景)**:
由于 JWT 无状态,我通过“Redis 黑名单 + 版本号”解决:
- 每个用户生成 JWT 时,加入 `version` 字段(初始为 1),并存入 Redis(key:user:{id}:jwt_version,value:1)。
- 用户修改密码时,将 Redis 中的 version 加 1。
- 拦截器解析 JWT 后,对比 JWT 中的 version 与 Redis 中的 version,不一致则判定 Token 失效,返回 401。
- 额外设置 Redis 黑名单,将过期/失效的 Token 存入(key:blacklist:{jwt},value:1,过期时间与 JWT 一致),拦截器先校验黑名单。
#### 身份无缝切换?
**答:**
同一个账号拥有用户 + 骑手两种身份。
前端切换身份 → 更新 Token → 后端重新解析身份 → ThreadLocal 覆盖 → 权限立即生效。
状态保存在 Token 中,保证一致性。
### 2. 幂等性拦截机制
#### 弱网重复提交场景?
**答:**
- 按钮连续点击
- 网络卡顿重试
- 接口超时重试
- 页面刷新重复提交
#### MD5 指纹幂等实现?你的 MD5 指纹是由哪些参数构成的?拦截逻辑是放在拦截器里还是AOP里?Redis 里面是怎么原子性判断重复的?
**答:**
1. **MD5 指纹构成**:
指纹 = MD5(用户ID + 请求URI + 核心业务参数(如订单ID) + 时间戳),其中:
- 用户ID:区分不同用户,避免跨用户冲突。
- 请求URI:区分不同接口。
- 核心业务参数:保证同一业务操作的唯一性(如订单ID)。
- 时间戳:限制幂等有效期(如 5 分钟)。
1. **拦截逻辑位置**:
放在 **AOP 切面**(而非拦截器),原因:
- 拦截器只能拿到 Http 请求参数,无法获取业务层的参数/返回值。
- AOP 可精准切入指定注解(如 `@Idempotent`)的方法,灵活控制幂等范围,且能获取方法入参。
1. **Redis 原子性判断**:
使用 `SETNX` 命令(原子操作):
- 指纹作为 Redis Key,值为 1,过期时间 = 幂等有效期(如 5 分钟)。
- 执行逻辑:`SETNX key 1 EX 300 NX`(不存在则设置,过期时间 300 秒)。
- 返回 1:首次请求,放行;返回 0:重复请求,拦截。
#### 极端情况:第一笔请求由于服务器满负载执行得很慢(超过了你的Redis过期时间),第二笔一样的请求过来了拦截器刚好放行,怎么办?
**答:**
我通过“Redis 过期时间兜底 + 数据库唯一索引”双层保障:
1. **延长 Redis 过期时间**:将过期时间设置为业务最大执行时间的 2 倍(如业务最多执行 5 分钟,过期时间设为 10 分钟),降低超时概率。
2. **数据库唯一索引**:在核心业务表(如订单表)添加唯一索引(如 user_id + order_type + create_time),即使 Redis 拦截失效,数据库也会拒绝重复插入,抛出唯一键冲突异常。
3. **业务层重试策略**:捕获唯一键冲突异常后,返回“操作已提交,请稍后查询结果”,避免用户重复操作。
### 3. WebSocket 实时推送 & Redis 坐标缓存
#### WebSocket 流程?断线重连?集群推送问题:WebSocket不能跨服务器通信!如果你的 SpringBoot 是部署了2个节点的,骑手通过WebSocket连接在A节点,用户通过WebSocket连在了B节点,A节点怎么把坐标推给B节点上的用户?
**答:**
1. **WebSocket 基础流程**:
建立连接 → 身份认证 → 维持长连接 → 服务端主动推送 → 客户端监听消息。
断线重连:前端监听关闭事件,定时重试(指数退避策略),重连后同步最新状态。
1. **跨节点推送解决方案**:
我结合 **Redis Pub/Sub(发布订阅)** 实现跨节点通信:
- 每个服务节点启动时,订阅 Redis 的指定频道(如 `rider_location_{订单ID}`)。
- 骑手坐标更新时,A 节点将坐标数据发布到 Redis 对应频道。
- B 节点订阅到消息后,通过本地 WebSocket 连接推送给用户。
- 核心逻辑:Redis 作为消息中转站,解决 WebSocket 连接与服务节点绑定的问题;高并发场景可替换为 RocketMQ/Kafka 提升可靠性。
#### Redis 骑手坐标存储?轨迹流:Redis 是用什么数据结构缓存的坐标流?高频坐标上报会压垮网络吗?前端做了什么样的平滑处理?
**答:**
1. **坐标缓存数据结构**:
- 实时坐标:用 Hash 结构(key:`rider:location:{骑手ID}`,field:lng/lat/updateTime,value:经纬度/更新时间),方便单点更新/查询。
- 轨迹流:用 ZSet 结构(key:`rider:track:{骑手ID}`,score:时间戳,value:经纬度字符串),按时间戳排序,方便查询历史轨迹。
2. **高频上报优化**:
- 服务端限流:骑手端每 2 秒上报一次坐标(而非实时上报),降低网络/Redis 压力。
- 数据压缩:经纬度保留 6 位小数,转为字符串后压缩传输。
- 批量推送:服务端将 1 秒内的坐标更新合并,批量推送给用户,减少 WebSocket 消息数。
3. **前端平滑处理**:
- 轨迹插值:若坐标上报中断,用贝塞尔曲线/线性插值补全轨迹,避免地图上“跳点”。
- 节流渲染:前端每 500ms 渲染一次坐标,而非收到消息立即渲染,降低页面卡顿。
### 4. 分布式锁 & 数据库乐观锁
#### Redis 分布式锁原理?如何避免死锁?你是用原生Redis的 SETNX 做的,还是用 Redisson?如果是 SETNX,业务代码没执行完,Redis锁超时自动释放了怎么办?
**答:**
1. **基础实现(SETNX)**:
核心命令:`SET lock:{订单ID} {唯一值} NX PX 30000`(NX:不存在则设置,PX:30 秒过期),解锁时用 Lua 脚本校验唯一值,避免误删其他线程的锁。
1. **死锁避免**:
- 设置过期时间,即使服务宕机,锁也会自动释放。
- 解锁操作放在 finally 中,确保业务执行完必解锁。
2. **锁超时问题(业务未执行完锁释放)**:
初期用原生 SETNX 时,我通过 **Redisson 看门狗机制** 解决:
- 替换为 Redisson 分布式锁,其内置“看门狗”线程:
- 锁默认过期时间 30 秒。
- 业务未执行完时,看门狗每 10 秒自动续期,延长锁过期时间。
- 业务执行完/服务宕机,看门狗停止,锁自动过期释放。
- 兜底方案:业务代码添加“操作状态”字段(如 processing/finished),即使锁超时,其他线程拿到锁后,检查状态为 processing 则等待/重试,避免重复执行。
#### 乐观锁实现?如果双11期间100个人同时给这个账户打钱,全靠乐观锁重试会导致大量的数据库压力或者请求失败,怎么优化?
**答:**
1. **乐观锁基础实现**:
表加 version 字段,更新语句:`update table set amount=amount+10, version=version+1 where id=? and version=?`,版本号不匹配则更新失败,业务层重试。
1. **高并发打钱场景优化**:
单纯乐观锁重试会导致“自旋重试”,增加 DB 压力,我采用:
- **Redis 预扣减 + 批量落库**:
1. 用户打钱时,先在 Redis 中预扣减金额(INCR/DECR),记录流水(List 结构)。
2. 后台线程批量将 Redis 流水同步到数据库(每 100 条/5 秒),更新账户余额。
3. 前端查询余额时,优先查 Redis(缓存 + 预扣减金额),保证实时性。
- **状态机控制**:
给账户添加“冻结/可用”状态,高并发时先冻结金额,批量处理完成后解冻,避免并发更新冲突。
- **熔断降级**:
设置重试次数上限(如 3 次),超过则返回“系统繁忙,请稍后再试”,避免无限重试压垮数据库。
#### 资金一致性?
**答:**
- 订单状态机控制,只能单向流转。
- 资金变动加分布式锁 + 乐观锁。
- 操作记录日志,可对账。
- 关键操作异步补偿、定时任务核对。
### 5. AI 多模态大模型应用
#### OCR 身份认证、NLP 地址解析?
**答:**
调用 AI 接口识别身份证信息,自动填充。
地址通过 NLP 解析省市区街道、门牌号,结构化存储。
相比传统规则,复杂场景准确率大幅提升,达到约 98%。
## 项目二:24小时在线校园智能助理
### 1. RAG 知识库
#### LangChain4j 索引流程?分块策略:处理《学生手册》时,Chunk Size(块大小)和 Overlap(重叠度)是怎么设置的?遇到表格或者复杂的目录格式怎么切分的?
**答:**
1. **基础索引流程**:
文档上传 → 文本分块 → 向量化 → 存入向量库 → 用户问题向量化 → 语义检索 → 召回相关片段 → 送给大模型生成回答。
1. **分块策略**:
- Chunk Size(块大小):设置为 512 个字符(适配主流嵌入模型的上下文窗口),保证单块信息完整且不超限。
- Overlap(重叠度):设置为 64 个字符,避免切割导致语义断裂(如一句话被切到两个块)。
- 表格/复杂目录处理:
- 表格:先转为 Markdown 格式,按行/列拆分,保留表头+行数据作为一个块,避免表格结构丢失。
- 目录:按章节/层级拆分,每个章节作为独立块,块元数据中记录章节号、层级,方便检索时定位上下文。
#### 检索优化:语义匹配使用的是什么向量数据库?什么叫 Top-K 筛选?如果召回的 Top-K 包含相互矛盾的文档(比如15年的旧校规和23年的新校规),大模型胡说八道(幻觉)怎么控制?
**答:**
1. **向量数据库选型**:
选用 **ChromaDB**(轻量级、易部署,适配校园项目),本地部署,无需额外云资源,支持快速的向量检索和元数据过滤。
1. **Top-K 筛选**:
Top-K 即“返回相似度最高的 K 个文档块”,我设置 K=5(兼顾召回率和推理效率),只将最相关的 5 个块送入大模型,避免上下文过长导致的幻觉/效率问题。
1. **矛盾文档/幻觉控制**:
- **元数据权重**:给每个文档块添加元数据(如生效时间、版本号),检索时优先筛选最新版本(如 23 年校规),旧版本(15 年)仅作为补充。
- **Rerank 重排序**:先用向量检索召回 Top-10,再用 BERT 重排序模型,基于语义+时间权重重新排序,取 Top-3 送入大模型。
- **提示词约束**:在 System Prompt 中明确“只使用检索到的最新文档回答,未找到则说‘暂无相关信息’,不编造内容”。
### 2. Agent 意图决策
#### 如何集成空教室查询?大模型是怎么知道要去调用你们业务系统的“查询空教室”接口的?
**答:**
1. **空教室查询集成**:
Agent 识别意图 → 调用内部服务接口 → 获取结果 → 整理成自然语言返回。
意图识别错误时:反问用户、提供可选菜单、引导澄清问题。
1. **Function Calling 实现逻辑**:
核心基于 **ReAct 范式 + Schema 定义**:
- 第一步:定义函数 Schema(JSON 格式),包含接口名称、入参(如校区、教学楼、时间段)、出参、描述。
- 第二步:将 Schema 传入大模型,Prompt 中明确“如需查询空教室,必须调用该函数,返回标准 JSON 参数”。
- 第三步:大模型解析用户问题(如“明天上午10点一教有没有空教室”),提取入参(校区:本校,教学楼:一教,时间段:明天10点),返回符合 Schema 的 JSON。
- 第四步:后端解析 JSON,调用空教室接口,将结果返回给大模型,大模型整理成自然语言回复用户。
### 3. 多模态审核
#### 视觉大模型审核?
**答:**
对图片、文本进行违规识别。
构建动态审核规则库:
- 政治敏感
- 色情暴力
- 广告骚扰
实时拦截,同时平衡准确率与响应速度。
### 4. 长会话 & 性能优化
#### Redis 多轮对话记忆?
**答:**
用户ID 作为 key,对话历史存在 Redis。
每次请求带上历史,实现多轮理解。
#### SSE 流式输出?首屏 <450ms?既然上一个项目用了 WebSocket,为什么大模型这个项目却用 SSE (Server-Sent Events) 返回呢?
**答:**
1. **SSE 流式输出优化**:
SSE 服务端单向推送,边思考边返回。
优化:异步检索、缓存高频问答、精简提示词、快速首句输出,达到首屏响应 <450ms。
1. **SSE vs WebSocket 选型原因**:
- WebSocket:全双工通信,适合双向实时交互(如跑腿平台的坐标推送、订单状态双向通知),但协议复杂,需维护长连接。
- SSE:单向通信(服务端→客户端),协议轻量(基于 HTTP),无需握手/心跳,适配大模型“打字机效果”(仅服务端推送回答,无客户端实时输入交互)。
- 核心权衡:大模型场景只需单向推送,SSE 开发成本更低、资源占用更少,且无需处理双向通信的异常(如断连、重连),更贴合业务需求。
### 5. 提示词工程
#### 如何优化提示词、避免幻觉?
**答:**
- 明确角色、任务、约束。
- 提供 Few-shot 示例。
- 强制只参考检索内容。
- 加上“不知道就说不知道”。
- 增加事实校验、来源引用。
大幅降低幻觉率。
# 四、通用面试题
### 自我介绍
**答:**
我是 Java 后端开发,熟练使用 Spring Boot、MySQL、Redis、MyBatis/MyBatis-Plus,有实习和完整项目经验。
实习中做过 CI/CD、报表优化、SQL 索引治理;
项目做过校园跑腿(实时推送、分布式锁、幂等)、校园智能助理(RAG、Agent、流式输出)。
注重工程化、性能与稳定性,能快速落地业务需求。
### 职业规划
**答:**
1~2 年深耕 Java 后端、分布式、微服务;
长期往架构/技术专家方向,负责系统设计与性能优化,持续学习云原生与 AI 应用落地。
### 遇到最大困难 & 怎么解决?
**答:**
例:大报表导出 OOM。
排查:定位全量加载导致内存溢出。
方案:流式查询 + EasyExcel 流式写入。
结果:解决 OOM,支持 10W+ 导出,接口稳定。
### 反问面试官(可直接用)
**答:**
- 团队目前技术栈和迭代节奏?
- 新人培养和成长路径?
- 这个岗位主要负责哪些业务/系统?
---
### 总结
1. 核心深挖点集中在**性能优化(OOM/慢SQL)、高并发(分布式锁/幂等)、AI落地(RAG/Agent/SSE)** 三大方向,答案需贴合项目场景,突出实操细节;
2. 技术选型类问题(如 ES vs MySQL 全文索引、SSE vs WebSocket)需强调“权衡思维”,结合业务规模/成本/复杂度说明选择依据;
3. 底层原理类问题(如 MyBatis 流式查询、ThreadLocal 内存泄漏)需讲清“原理+解决方案+工程兜底”,体现闭环思维。
> (注:文档部分内容可能由 AI 生成)