靖安科技一面:Java 全链路 97 问与无人机平台深挖
- 轮次
- 一面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- 请做自我介绍。
- 上一段实习为什么离职?
- 聊天列表去重后的用户编码,根据编码查用户信息,用什么集合?
- 多个请求同时修改一个 HashMap 会有什么问题?
- 数据库只剩一个商品,两个人同时下单,程序中查到还有库存怎么办?
- 这个锁你会怎么加?
- 创建订单接口:接口请求、业务判断、写数据库分别放在哪?
- 业务比较简单(二三十行),还放 service 还是放 controller?
- 你第一个学的语言是 Java 吗?
==和equals的区别?equals和hashCode?- String 源码看过吗?
- String 是什么修饰的?
- 类能被重写吗?
- String 是线程安全的吗?
- StringBuffer 和 StringBuilder 的 append 都是线程安全的吗?
- synchronized 可重入吗?
- synchronized 的底层原理?
- 订单创建成功但积分扣失败,怎么避免订单和积分状态不一致?
- 加了
@Transactional注解,什么情况下会失效? - MySQL 索引 + EasyExcel 分批导出 + 线程池,讲讲?
- 为什么不用 Apache POI?
- 为什么 EasyExcel 这么快?
- 要导出几百 MB 的设备日志文件,你怎么设计导出方式?
- 商品价格存 MySQL + Redis 缓存,修改价格后怎么避免用户看到旧价?
- 5000 份文档的切片与向量化,讲讲?
- 切片策略是什么?
- 用的什么向量数据库?
- 为什么不用 ES 做语义检索?
- 58 → 92 这个指标是怎么来的?
- 保存 10 万条聊天记录,支持按位置读取、末尾添加删除,用什么数据结构?
- 根据消息 ID 查到消息再删除,用列表一定更快吗?
- HashMap 为什么很快?
- HashMap 怎么遍历?
- 1000 万个接口耗时已知,查最慢的 100 条,会全部排序吗(空间只给 100)?
- 最大堆 / 最小堆怎么实现?
- 运维知识库是 QA 助手还是 Agent?
- 一个问题从开始到结束,系统内部都做什么?
- 意图识别怎么做?
- 为什么用 WebSocket + Protobuf(二进制协议)?
- TCP 和 UDP 的区别?
- 文件传输、实时语音分别更看重什么?
- 什么是跨域?
- 只配置 CORS 能阻止别人脚本调用接口吗?
- Cookie、Session、JWT 分别是什么?
- Cookie 存在哪、什么时候发送?
- Cookie 的发送规则?
- JWT 里能放密码吗?
- 用户退出后,原来的 JWT 会自动失效吗?
- 每进来一个请求创建一个线程、处理完销毁,有什么问题?
- 怎么解决?
- 请求一直进来、处理速度跟不上怎么办?
- 有什么措施?
- 用过什么类似的工具(线程池)?
- 线程池有哪些参数?
- 拒绝策略实现在哪个类?
- 一个线程调远程接口等 3 秒返回,这 3 秒一直占 CPU 吗?
- 1000 个请求都在等待,加 CPU 核心有用吗?
- 正在计算和正在等待有什么区别?
- 一台机器 8 个 CPU,怎么能跑很多线程?
- 图片压缩和图片下载(CPU 密集 vs IO 密集)适合的线程数一样吗?
- final 关键字?
- final 修饰一个 List,能对它 add 吗?
- Redis 预减库存 + Lua 原子脚本防超卖 + RocketMQ,讲讲?
- Redis 有几种数据结构?
- RocketMQ 有什么持久化策略?
- MySQL 最左前缀法则?
- 给 ABC 建联合索引,只查 B、C 会用这个索引吗?
- 写一个 update 语句:改某个用户的电话。
- 前端点击超时后再次点击创建订单接口怎么办?
- 怎么避免重复提交?
- AI 回答用 SSE 还是 WebSocket,为什么?
- 后端 SSE 分段推、前端一次性全出来,什么原因?
- 用户点发送一直转圈,按什么顺序排查?
- 说一下你实际优化过的接口、业务用途?
- 3.2 秒主要卡在哪?
- 卡在数据库还是外部接口还是等待?
- 用什么工具判断瓶颈?
- Redis 里具体存了什么?
- 线程池异步具体在哪一步执行?
- 是多个查询一起并行执行吗?
- 600 毫秒是业务已经完成了吗(落库?)?
- 无人机平台的整体架构?
- 系统有哪些能力?
- 10 万架无人机是对象存在内存里?
- 每台设备多久更新一次?
- 发过来的消息带什么?
- MQTT 了解吗?
- 物模型有概念吗?
- Spring Boot 的 application 注解 / 配置文件加载过程?
- 仿真引擎的视野驱动推送怎么实现?
- 对角线(视野范围)怎么过滤?
- 全部遍历一遍吗(空间索引)?
- 无人机飞进视野了怎么办?
- 开 100 个前端会怎么样?
- 线性插值算轨迹、急转弯 / 加减速不合理轨迹怎么处理?
- 这是轨迹预测吗?
《参考解析》
订单与积分的一致性,以及 @Transactional 为什么会失效
跨表/跨服务的「订单成功但积分失败」本质是分布式一致性问题,先分清两种取舍:如果积分只是附属权益,就走最终一致——订单本地事务里同时写一条积分流水(或本地消息表),事务提交后由定时任务或 MQ 异步扣积分,失败重试 + 幂等(用业务唯一键去重),再加一个 T+1 对账任务捞出差额人工/自动补偿;如果必须强一致且都在同一个库,就把订单和积分放进同一个本地事务,用行锁或乐观锁版本号保证并发正确。真正要避免的是「先远程调积分服务、成功了再写订单」这种没有补偿的写法。
@Transactional 失效的典型场景:① 方法不是 public(Spring AOP 代理拦不到)或同类内部方法自调用(走的是 this,绕过了代理);② 异常被自己 catch 掉没抛出去,或抛的是受检异常而默认只回滚 RuntimeException/Error(需要 rollbackFor = Exception.class);③ 传播行为设成 REQUIRES_NEW/NOT_SUPPORTED 导致当前事务被挂起;④ 多数据源没配对应的事务管理器,或者事务管理器与实际操作的库不匹配;⑤ 存储引擎是 MyISAM 这类不支持事务的;⑥ 注解打在接口上而代理模式是 CGLIB,或者类没被 Spring 管理。另外要提醒一个常被忽略的点:事务里做远程调用/发 MQ,会拉长事务持有时间,正确做法是事务内只落库、事务提交后再发消息(TransactionSynchronization 或本地消息表)。
MySQL 与 Redis 的价格缓存怎么保持一致
先想清楚「不一致能容忍多久、错价的代价有多大」。可行的方案按代价从低到高:Cache Aside + 先更新数据库再删缓存(不是更新缓存),并给缓存加过期时间兜底;删除失败时用消息队列重试删除,或用 binlog 订阅(Canal)异步刷新,把「删缓存」变成一个可靠事件。之所以是删除而不是更新,是因为并发写时两个请求的更新顺序无法保证,容易出现旧值覆盖新值的长尾脏数据。
更稳的做法是给价格加版本号或时间戳:读取缓存时带上版本,写入时版本更高的才能覆盖,读到低版本就丢弃重载;对价格这类强敏感数据,可以在 Redis 里只放「价格版本 + 短 TTL」,真正的价格从 DB 读,用很小的读放大换正确性。若要求绝对实时,就让价格读走 DB(或只读从库)不走缓存,把缓存让给商品描述这类不敏感字段。面试官常追问「延迟双删」——它只能缩小不一致窗口,不能消除,所以正解还是「删除 + 版本/过期 + 变更事件」,而不是靠 sleep。
从「每请求一个线程」到线程池:参数、拒绝策略与线程数怎么定
每请求新建线程的问题:创建/销毁本身有开销(内核态登记、栈内存分配),线程数不可控会打爆内存和上下文切换,且没有排队机制——流量峰值时不是变慢而是直接雪崩。解法是线程池:复用线程、用队列做缓冲、用拒绝策略做背压。
参数上要讲清「提交任务后的顺序」:核心线程 → 入队 → 扩到最大线程 → 拒绝。队列用无界 LinkedBlockingQueue 会让最大线程数失效并有 OOM 风险,所以生产上要有界;拒绝策略里 AbortPolicy 直接抛异常(默认)、CallerRunsPolicy 让提交线程自己跑形成背压、DiscardPolicy/DiscardOldestPolicy 会静默丢任务,一般不用(丢任务至少要留日志)。线程数的经验值:CPU 密集型 ≈ 核数 + 1,IO 密集型 ≈ 核数 × (1 + 等待时间/计算时间),但公式只是起点,必须压测。这也能解释面试里的连环问:一个线程等远程接口 3 秒期间不占 CPU(线程被挂起在等待队列,处于 WAITING/TIMED_WAITING),所以 1000 个等待请求加 CPU 核数意义不大,真正该加的是线程数或改成异步/响应式;而 8 核能跑很多线程,也正是因为大部分线程在等待而不是在计算。
synchronized 的可重入与底层原理
可重入指的是同一线程已经持有某对象的锁,再进入该对象的另一个 synchronized 方法/代码块时不会被自己阻塞,实现上靠对象头里的锁记录计数:每次重入计数加一,退出时减一,减到零才真正释放。它的意义在于避免自死锁——比如 synchronized 方法里调用同类的另一个 synchronized 方法。
底层原理要分层次答:字节码层面,同步代码块编译成 monitorenter/monitorexit(异常路径也会有一对保证释放),同步方法用方法访问标志 ACC_SYNCHRONIZED;对象层面,锁信息记录在对象头的 Mark Word 里,JVM 为了降低开销做了锁升级——偏向锁(同一线程反复进入,只在 Mark Word 记线程 ID,JDK 15 后默认关闭并逐步废弃)→ 轻量级锁(CAS 把 Mark Word 指向栈上的锁记录,适合无竞争或短时间自旋)→ 重量级锁(竞争激烈时向操作系统申请 mutex,未抢到的线程进入等待队列,涉及用户态/内核态切换)。要点是:升级不可逆、锁的是对象而不是代码、静态 synchronized 方法锁的是 Class 对象、synchronized 与 wait/notify 必须配合同一把锁使用;面试常追问它和 ReentrantLock 的区别——后者可中断、可超时、可公平、支持多个条件队列,代价是要手动 unlock。
SSE 与 WebSocket 怎么选,以及「后端分段推、前端一次性出来」的原因
选型先看通信方向与需求:SSE 是服务器到客户端的单向流,基于普通 HTTP(Content-Type: text/event-stream)、自带断线重连(Last-Event-ID)和事件 ID,实现简单、天然穿过大多数代理和网关;WebSocket 是全双工,适合需要客户端持续上行低延迟数据的场景(协同编辑、实时语音、游戏),代价是要处理握手升级、心跳保活、连接管理和扩容。大模型对话的典型形态是「一次提问、服务端持续下行」,所以 SSE 更合适;需要双向交互或多路复用时才上 WebSocket。
「后端已经分段推、前端却一次性全出来」几乎都是缓冲造成的,按概率排查:① 反向代理/Nginx/CDN 开了 proxy_buffering 或响应压缩,把小块攒成大块再下发(对应关掉缓冲、禁用对 text/event-stream 的 gzip);② 应用层没关输出缓冲或框架把响应包成了 JSON 一次性返回(比如用了普通 @RequestMapping 返回对象,而不是 SseEmitter/Flux);③ 前端用 fetch 拿完整 text() 再解析,而不是 EventSource 或流式读取 response.body;④ 服务端虽然分了段,但每段之间没有真正 flush,或者业务在等模型全部生成完才吐。定位手段很直接:先 curl -N 看服务端是不是真的逐块下发,再逐层加上代理看在哪一层被攒住,最后查前端读取方式。