每块给四段:30 秒电梯陈述(开场抛出)→ 3 分钟深讲(被追问时展开)→ 追问防御(面试官可能的下一问)→ 关键事实(背死的硬数字/原理,别记错)。
总原则:给方法不给数字、讲真实踩坑不讲概念、每个点都绑一个项目里的具体场景。下面所有类名已泛化,但技术原理和版本号是真实的。
主题 1:连接池僵尸连接事故(数据库深度 · 王牌弹药)
【30 秒电梯陈述】
「生产出过一次连接池雪崩。根因是 Spring Boot 2.3.12 默认带的 HikariCP 3.4.5 不支持 keepaliveTime,没法主动探活空闲连接。我们部署在四层负载均衡器后面,加上 MySQL wait_timeout 会异步杀空闲连接,池子里就会拿到僵尸连接——应用拿一条死连接去发请求,直接超时。我把 HikariCP 升到 4.0.3,它支持 keepaliveTime 主动探活,僵尸连接在借出前就被清掉。」
【3 分钟深讲】
- 现象:间歇性
Connection is not available,白天流量小没事,夜里/周末连接长时间空闲后,早高峰一来批量超时。特征是「重启就好、过几小时又犯」。 - 根因链:
- 部署在四层负载均衡器后,连接是 TCP 转发,连接的死活应用层感知不到。
- MySQL 的
wait_timeout会异步杀空闲连接。 - HikariCP 3.4.5 没有
keepaliveTime(3.5.0 才引入),只能靠maxLifetime被动到点重建。maxLifetime是寿命上限不是探活——连接在maxLifetime之前被对端杀了,池子不知道,下次借出就是僵尸。
- 解决:升级到 HikariCP 4.0.3。理由写在 pom 注释里:4.0.3 兼容 Spring Boot 2.3-2.5、Java 11+、JDBC 4.2,已在生产 2.5 项目大规模验证。开了
keepaliveTime后,池子主动周期性发探活查询,死连接提前被剔出。 - 配套:
maximum-pool-size=20不是随手填的,是为了防某个 stop 操作卡死实测调出来的,别随手调小。
【追问防御】
- 为什么不直接升 Spring Boot? 升框架是大动作、要全量回归。只升一个组件、用 4.0.3 这个兼容版本,是最小爆炸半径的修复。工程上优先选「最小可逆的修复」而不是「顺带升级」。
keepaliveTime和maxLifetime区别?maxLifetime是连接最大存活时间,到点强制重建(被动);keepaliveTime是探活周期,每隔这么久对空闲连接发保活查询,发现死了提前剔出(主动)。一个是寿命上限,一个是健康检查。- 为什么不换 Druid? HikariCP 性能更好、Spring Boot 默认集成,监控用 Prometheus 补,没必要为监控换池子。
- 怎么定位是僵尸连接? 看 HikariCP 的 MBean(
pool.idlevspool.active),对比 MySQL 侧SHOW PROCESSLIST存活连接数。应用以为有 N 条可用、实际 MySQL 侧早没了,就是僵尸。
【关键事实】
- 升级路径:HikariCP 3.4.5 → 4.0.3
- 关键能力:
keepaliveTime(3.5.0 引入) - 部署:四层负载均衡 + MySQL
wait_timeout maximum-pool-size=20(防 stop 卡死,勿调小)
主题 2:漏斗状态机恒等约束(业务建模 · 复杂度天花板)
【30 秒电梯陈述】
「我做的需求漏斗有 6 个联动节点——需求→issue→采集→质检→回传→完成。它有个恒等约束:质检成功 ⟹ 采集成功,质检失败 ⟹ 采集一定失败,所以 回传 ⊆ 质检 == 采集,‘采集成功但质检失败’这种状态在我们系统里不存在。我把节点联动收拢在一个 Service 里,用单判据驱动多个节点的百分比恒等,只让展示时间不同。」
【3 分钟深讲】
- 状态机全貌(status 记的是”已完成的哪一步”,当前站在下一步):
CREATED(1)待运营补充 →OPS_SUPPLEMENT(2)待运营确认 →OPS_CONFIRM(3)待需求方确认 →REQ_CONFIRM(4)待下发 →5需求下发 →6-9采集/质检/回传/完成 →10已完成 →11已取消
- 两个易错点(讲出来就是资深味):
- 状态枚举和流转节点枚举的 3/4 含义互换:一个枚举里 code 3 是「运营确认」、4 是「需求方确认」;另一个枚举反过来。写流转记录必须照抄现有同类动作的取值,不能按名字推。
- status 是”已站在下一步”:驳回/取消还原留痕节点时,要映射到 status 的下一环,不是同名平移。叠加 3/4 互换,看起来像”跳序”,其实是顺推一环。
- node 5-9 不写进度表:node5(需求下发)不主动确认,只置
status=5、不写 progress;点亮时间由漏斗按关联任务的最早创建时间推导。node6-9 同理全靠漏斗推导。往进度表补写这些节点会和漏斗推导打架。 - partial(部分达成):partial 节点照常写时间——这是”放行后续节点”的实现方式;但最终完成节点额外要求无一节点为 partial。
- 轨迹质检不是独立节点:它是并入数据质检的一个判据条件,且只对 issue 链生效。搬错会让常规链数据质检永久 0%。
【追问防御】
- 为什么不让 status 直接表示当前节点? 那样”已完成”和”进行中”会混在一个值里。我们的设计是 status=已完成的那一步,下一步是 status+1。好处是查询”卡在哪一步”很直接,坏处是映射留痕节点时要顺推一环。
- 用状态表还是枚举硬编码? 枚举定义状态空间,流转规则集中在一个 Flow Service 里管理。没上状态机引擎——节点数有限、流转规则强约束,硬编码更可读好测。引擎是节点多、流转灵活才值得上。
- parent status=5 / 子 issue status=4 是 bug 吗? 不是。父走流转、子靠漏斗推导,两条线不耦合,刻意设计,别修。
【关键事实】
- 节点:1-4 流转 + 5 下发 + 6-9 漏斗推导 + 10 完成 + 11 取消
- 恒等式:
回传 ⊆ 质检 == 采集 - 核心:漏斗 Service + Flow Service 的 doDispatch
主题 3:点云预览与请求调度(前端工程深度 · 差异化弹药)
【30 秒电梯陈述】
「自动驾驶点云预览,我支持 pcd/drc/potree 三种格式。potree 这套的难点不是渲染,是请求调度——potree-core 原生 fetch 不带鉴权,对象存储又要签名 URL,还有 Range 分片和大小降级。我写了个签名请求管理器:把 potree 内部请求重写到一个逻辑源,由管理器解析成签名 URL 并注入 token、X-Request-ID、X-User-Name;完整响应缓存复用做 Range 切片;octree 超过 50MB 抛降级错误。」
【3 分钟深讲】
- 三种格式三种 loader:
pcd→ three 的 PCDLoaderdrc→ DRACOLoader,且全局单例,setWorkerLimit(2)限制 worker,preload()预加载解码器——避免每次预览都起 worker 池potree→ potree-core,按需加载八叉树分片
- 渲染性能控制:
POTREE_POINT_BUDGET = 2_000_000,限制渲染点数- 像素比限制在
[1, 2],防高 DPI 设备(3x retina)渲染过重 - 自适应点大小:按包围球半径算
- 着色按属性自动选:有
rgba/rgb→ RGB;有intensity→ INTENSITY_GRADIENT;否则默认色
- 请求调度的真正难点:
- 三种资源:
metadata.json/hierarchy.bin/octree.bin - 签名 URL 复用:用 Promise Map 缓存,同一资源只签名一次(签名接口有成本)
- 完整响应复用做 Range:对象存储不支持 Range、或 octree 超阈值时,从内存缓存切片构造 206 响应
- 50MB 降级:
POTREE_FULL_RESPONSE_LIMIT_BYTES = 50MB,超过抛降级错误——不让一个超大文件把浏览器内存打爆 - 重试:
MAX_RETRIES=2,仅对 429/5xx 重试,退避attempt * 150ms(线性,非指数)
- 三种资源:
- 坐标转换(自动驾驶领域硬技能):
proj4做 UTM ↔ WGS84 投影- 自写 GCJ02(火星坐标系)偏移算法,带
out_of_china判断——国内地图合规必需 - 链路:UTM → WGS84 → GCJ02(渲染到国内地图)
【追问防御】
- 为什么重写 URL 到逻辑源? potree-core 内部用 fetch 加载分片,URL 构造无法改,但能拦截请求。把所有 potree 请求重定向到一个不存在的逻辑源,识别出是哪个资源再换签名 URL——不改 potree 源码就注入了鉴权。
- 为什么用完整响应切片做 Range? 签名 URL 的对象存储 Range 支持不稳定(取决于 provider)。与其赌它支持,不如客户端缓存完整响应(octree 通常几十 MB 可控),后续 Range 请求本地切片。代价是首次下载完整文件,收益是 Range 行为可预期。超过 50MB 才降级报错。
- 点云卡顿怎么排查? 先看点预算是不是超了(200 万是经验上限),再看像素比是不是被高 DPI 拉到 3,最后看是不是同时加载了多个点云没释放。
- DRACOLoader 为什么单例? DRACO 解码跑在 Web Worker 里,每次 new 会起新 worker 池。单例 + worker 限制保证全应用复用解码器和 wasm 内存。
【关键事实】
- 3 格式:
pcd/drc/potree - 关键常量:
POTREE_POINT_BUDGET=2_000_000、POTREE_FULL_RESPONSE_LIMIT_BYTES=50MB、MAX_RETRIES=2 - 坐标:
proj4+ 自写 GCJ02,链路 UTM→WGS84→GCJ02
主题 4:JPA 防 N+1(持久层 · 正面教材)
【30 秒电梯陈述】
「我们用 JPA 但实体之间不建对象关联,全部逻辑外键——从根上规避了懒加载 N+1。列表装配严格走『先收 id → findByIn 批量 → Map 索引内存装配』。比如需求列表要带进展、子 issue、漏斗、权限,八个子查询全是批量,代码注释里显式标注防 N+1。我修过一个真实的 N+1:建任务人姓名快照列没回填存量,详情页逐条回落查共库,我改成收集空快照的 taskId 一次性批量查,还加了『全是新数据就不发 SQL』的短路。」
【3 分钟深讲】
- 根上规避:实体没有任何
@ManyToOne/@OneToMany,关联用 id 字段表达。对象图 N+1 在这一层就不存在。 - 列表装配的八批量:
- 进展节点时间批量
- 跨库漏斗聚合批量
- 子 issue 计数批量
- 子 issue 列表批量
- 子 issue 进展批量
- 子行权限批量
- 审核人池(注释:「各算一次,避免逐行查库」)
- 统一模式:
collect id → findByIn(ids) → Map<id,entity> → 装配时 get(id) - 真实修复案例:
- 背景:建任务人姓名的快照列是后加的、不回填存量,老任务姓名全空
- 隐患:详情页逐条按 taskId 回落查共库 → N+1
- 修复:收集空快照的 taskId 集合 → 一次批量查 → Map 索引装配
- 优化:空集合时直接返空 map、不发 SQL(纯新数据零代价)
- 兜底:查询失败只 warn 不抛,姓名留空,不让详情页挂
【追问防御】
- JPA 的 N+1 怎么产生?
@ManyToOne/@OneToMany默认 LAZY,循环里访问关联属性每次触发一条 SQL。N 条主记录 → N+1 次查询。我们没建对象关联所以这层不触发;但手动 N+1(循环里按 id 逐条查)一样要防,靠批量 IN。 - 为什么不用
@EntityGraph/join fetch? 我们没建关联注解,join fetch无从 fetch。批量 IN 是等价手写 join fetch 的效果,但 SQL 自己控、不依赖 JPA 关联图生成。 - 真有隐患吗? 有一个
@OneToMany(fetch=EAGER)在组织树实体上,组织树一次性渲染才这么选。层级深了要改 LAZY + EntityGraph 按需抓取。这是我知道的待优化点,不是已爆的事故。
【关键事实】
- 实体层:0 个关联注解,全逻辑外键
- 统一模式:
collect id → findByIn → Map → get
主题 5(附加·边缘计算亮点):SSH 下发车端
【30 秒电梯陈述】
「采集任务要 SSH 到车端执行 shell 脚本下发。踩过两个硬坑:一是车端 bash 要求 LF 行尾,Windows 的 CRLF 会让车端报 command not found,我们用 .gitattributes 强制 shell 脚本 LF;二是车端外挂盘按车型解析(不同车型是 /dev/sda 或 /dev/nvme0n1),写死会格式化错误设备。还有一个业务红线:停止采集绝不『只改库不通知车』——历史上造成过库里已结束、车端仍在录的幽灵录制。」
【追问防御】
- SSH 还是 Kafka 下行? 两套并行实现,前端按开关灰度切换。SSH 版用 sshj,链路版用 Kafka 下行。灰度切换时两边的 start/stop/重派 都要落痕,否则操作历史会断档。
- 车端不可达怎么办? 停止失败/车不可达时按
采集失败收尾、任务照常解卡。无论 stop 成败,下发状态保持成功——回写失败会把「采集阶段失败」误标成「下发失败」,前端会错出「重新下发」按钮。
【关键事实】
- 核心:
.gitattributes强制 LF;按车型解析磁盘;幽灵录制事故
主题 6(附加·架构判断力):共库直读 vs Feign
【30 秒电梯陈述】
「两个服务之间不走 Feign/MQ,走共库直读/直写。这不是没设计,是判断——跨服务调用会放大故障域,内部系统里共库直读更简单可靠。写侧失败只告警不抛,因为跨服务快照表不能让它打断采集任务创建主流程。」
【追问防御】
- 这不违反微服务数据隔离吗? 严格微服务要求每服务独占数据。但我们是平台内强内部服务,数据所有权清晰(一方拥有表、另一方只读),共库直读是务实选择。代价是改表结构要两侧联动检查。这叫「该共库时不用 Feign」——知道什么时候不该教条。
- 什么时候必须上 Feign/MQ? 跨团队、跨域、需要独立扩缩容、或调用链路要独立熔断降级时。我们是同团队、同部署、同扩缩容,共库直读收益大于成本。
【关键事实】
- 共库表:需求 / 子 issue / 任务关联快照
- 原则:写侧失败只告警不抛
临场总原则(进场前再过一遍)
- 每个技术点都绑一个项目里的具体场景——不绑具体代码的答案都是背的。
- 给方法不给数字(除了自己实测调出来的常量,如
maximum-pool-size=20、POTREE_POINT_BUDGET=2_000_000)。 - 主动说坑点:面试官每追问一层,你早一步说「这里有个坑,我们是这么填的」。
- 诚实边界:没做过的别说做过;选型理由代码里没写的,说「这是团队当时的判断」而非「我的设计」。
- STAR 讲项目:Situation 一句业务背景 → Task 你的职责 → Action 一个具体决策和取舍 → Result 量化数字。
进场前最后一句:面试官要的从来不是「你知道这些」,而是「你在不确定性下兜得住」。连接池会拿僵尸、状态机会漂移、点云会卡顿、N+1 会慢——这些你都填过,就是全栈的价值。






