3723 字
19 分钟
一份可背诵的全栈面试话术卡:从连接池事故到点云调度

每块给四段:30 秒电梯陈述(开场抛出)→ 3 分钟深讲(被追问时展开)→ 追问防御(面试官可能的下一问)→ 关键事实(背死的硬数字/原理,别记错)。

总原则:给方法不给数字、讲真实踩坑不讲概念、每个点都绑一个项目里的具体场景。下面所有类名已泛化,但技术原理和版本号是真实的。

主题 1:连接池僵尸连接事故(数据库深度 · 王牌弹药)#

【30 秒电梯陈述】#

「生产出过一次连接池雪崩。根因是 Spring Boot 2.3.12 默认带的 HikariCP 3.4.5 不支持 keepaliveTime,没法主动探活空闲连接。我们部署在四层负载均衡器后面,加上 MySQL wait_timeout 会异步杀空闲连接,池子里就会拿到僵尸连接——应用拿一条死连接去发请求,直接超时。我把 HikariCP 升到 4.0.3,它支持 keepaliveTime 主动探活,僵尸连接在借出前就被清掉。」

【3 分钟深讲】#

  1. 现象:间歇性 Connection is not available,白天流量小没事,夜里/周末连接长时间空闲后,早高峰一来批量超时。特征是「重启就好、过几小时又犯」。
  2. 根因链
    • 部署在四层负载均衡器后,连接是 TCP 转发,连接的死活应用层感知不到。
    • MySQL 的 wait_timeout 会异步杀空闲连接。
    • HikariCP 3.4.5 没有 keepaliveTime(3.5.0 才引入),只能靠 maxLifetime 被动到点重建。maxLifetime 是寿命上限不是探活——连接在 maxLifetime 之前被对端杀了,池子不知道,下次借出就是僵尸。
  3. 解决:升级到 HikariCP 4.0.3。理由写在 pom 注释里:4.0.3 兼容 Spring Boot 2.3-2.5、Java 11+、JDBC 4.2,已在生产 2.5 项目大规模验证。开了 keepaliveTime 后,池子主动周期性发探活查询,死连接提前被剔出。
  4. 配套maximum-pool-size=20 不是随手填的,是为了防某个 stop 操作卡死实测调出来的,别随手调小

【追问防御】#

  • 为什么不直接升 Spring Boot? 升框架是大动作、要全量回归。只升一个组件、用 4.0.3 这个兼容版本,是最小爆炸半径的修复。工程上优先选「最小可逆的修复」而不是「顺带升级」。
  • keepaliveTimemaxLifetime 区别? maxLifetime 是连接最大存活时间,到点强制重建(被动);keepaliveTime探活周期,每隔这么久对空闲连接发保活查询,发现死了提前剔出(主动)。一个是寿命上限,一个是健康检查。
  • 为什么不换 Druid? HikariCP 性能更好、Spring Boot 默认集成,监控用 Prometheus 补,没必要为监控换池子。
  • 怎么定位是僵尸连接? 看 HikariCP 的 MBean(pool.idle vs pool.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 分钟深讲】#

  1. 状态机全貌(status 记的是”已完成的哪一步”,当前站在下一步):
    • CREATED(1) 待运营补充 → OPS_SUPPLEMENT(2) 待运营确认 → OPS_CONFIRM(3) 待需求方确认 → REQ_CONFIRM(4) 待下发 → 5 需求下发 → 6-9 采集/质检/回传/完成 → 10 已完成 → 11 已取消
  2. 两个易错点(讲出来就是资深味)
    • 状态枚举和流转节点枚举的 3/4 含义互换:一个枚举里 code 3 是「运营确认」、4 是「需求方确认」;另一个枚举反过来。写流转记录必须照抄现有同类动作的取值,不能按名字推。
    • status 是”已站在下一步”:驳回/取消还原留痕节点时,要映射到 status 的下一环,不是同名平移。叠加 3/4 互换,看起来像”跳序”,其实是顺推一环。
  3. node 5-9 不写进度表:node5(需求下发)不主动确认,只置 status=5不写 progress;点亮时间由漏斗按关联任务的最早创建时间推导。node6-9 同理全靠漏斗推导。往进度表补写这些节点会和漏斗推导打架。
  4. partial(部分达成):partial 节点照常写时间——这是”放行后续节点”的实现方式;但最终完成节点额外要求无一节点为 partial
  5. 轨迹质检不是独立节点:它是并入数据质检的一个判据条件,且只对 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 分钟深讲】#

  1. 三种格式三种 loader
    • pcd → three 的 PCDLoader
    • drc → DRACOLoader,且全局单例setWorkerLimit(2) 限制 worker,preload() 预加载解码器——避免每次预览都起 worker 池
    • potree → potree-core,按需加载八叉树分片
  2. 渲染性能控制
    • POTREE_POINT_BUDGET = 2_000_000,限制渲染点数
    • 像素比限制在 [1, 2],防高 DPI 设备(3x retina)渲染过重
    • 自适应点大小:按包围球半径算
    • 着色按属性自动选:有 rgba/rgb → RGB;有 intensity → INTENSITY_GRADIENT;否则默认色
  3. 请求调度的真正难点
    • 三种资源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(线性,非指数)
  4. 坐标转换(自动驾驶领域硬技能):
    • 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_000POTREE_FULL_RESPONSE_LIMIT_BYTES=50MBMAX_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 分钟深讲】#

  1. 根上规避:实体没有任何 @ManyToOne/@OneToMany,关联用 id 字段表达。对象图 N+1 在这一层就不存在。
  2. 列表装配的八批量
    • 进展节点时间批量
    • 跨库漏斗聚合批量
    • 子 issue 计数批量
    • 子 issue 列表批量
    • 子 issue 进展批量
    • 子行权限批量
    • 审核人池(注释:「各算一次,避免逐行查库」)
  3. 统一模式collect id → findByIn(ids) → Map<id,entity> → 装配时 get(id)
  4. 真实修复案例
    • 背景:建任务人姓名的快照列是后加的、不回填存量,老任务姓名全空
    • 隐患:详情页逐条按 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 / 任务关联快照
  • 原则:写侧失败只告警不抛

临场总原则(进场前再过一遍)#

  1. 每个技术点都绑一个项目里的具体场景——不绑具体代码的答案都是背的。
  2. 给方法不给数字(除了自己实测调出来的常量,如 maximum-pool-size=20POTREE_POINT_BUDGET=2_000_000)。
  3. 主动说坑点:面试官每追问一层,你早一步说「这里有个坑,我们是这么填的」。
  4. 诚实边界:没做过的别说做过;选型理由代码里没写的,说「这是团队当时的判断」而非「我的设计」。
  5. STAR 讲项目:Situation 一句业务背景 → Task 你的职责 → Action 一个具体决策和取舍 → Result 量化数字。

进场前最后一句:面试官要的从来不是「你知道这些」,而是「你在不确定性下兜得住」。连接池会拿僵尸、状态机会漂移、点云会卡顿、N+1 会慢——这些你都填过,就是全栈的价值。

一份可背诵的全栈面试话术卡:从连接池事故到点云调度
https://gilgameshzzz.github.io/posts/interview-scripting-card/
作者
Amadeus
发布于
2026-09-18
许可协议
CC BY-NC-SA 4.0