顺丰 Java 后端一面面经(无手撕)
- 轮次
- 一面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 你平常用多线程做了哪些任务?
- 多线程的核心参数有哪些?
- 多线程的任务加入流程是怎样的?
- 多线程的无界队列和有界队列怎么选择?
- 有哪些拒绝策略?
- 使用无界队列的时候发生 OOM 了怎么办?
- OOM 的排查过程是怎样的?
- 有哪些专业软件可以用来分析 OOM?
- Arthas 的使用方法是怎样的?
- Redis 为什么快?
- Redis 的分布式锁是怎么实现的?
- Redisson 是怎么进行锁续期的?
- 应用内存和 Redis 内存有什么区别?
- 如果让你设计,你怎么设计应用内存、Redis 和数据库之间的交互架构?
- 你的设计有什么缺点?
- MySQL 的 B 树和 B+ 树有什么区别?
- B+ 树相对于 B 树的缺点是什么?
- Kafka 的高水位线是什么意思?
- 介绍一下简历里的 Agent 项目。
- 怎么设计记忆模块?长期和短期记忆是怎么设计的?
- 上下文压缩是怎么设计的?
- LangGraph 中编排的流程是动态的还是线性的?
- 反问。
《参考解析》
线程池:参数、入队流程与队列选择。七个参数是 corePoolSize、maximumPoolSize、keepAliveTime 与 unit、workQueue、threadFactory、handler。任务提交顺序必须背准:核心线程未满就新建核心线程执行;核心满了入队;队列满了才扩到最大线程数;再满交给拒绝策略。因此队列类型直接决定行为:有界队列(ArrayBlockingQueue 或指定容量的 LinkedBlockingQueue)让扩容与拒绝生效;默认构造的无界 LinkedBlockingQueue 会让 maximumPoolSize 形同虚设,任务无限堆积直到 OOM,表现为「服务没报错但请求全部超时」。选型判据是「上游能不能等、下游能不能扛」:核心业务用有界队列加 CallerRunsPolicy 形成反压,可丢弃的旁路任务(埋点、日志)用有界队列加 DiscardPolicy 并打日志,不允许排队的用 SynchronousQueue 配较大的最大线程数。线程数按 CPU 密集型取「核数加一」、IO 密集型按「核数乘 (1 加等待比)」估算,最终以压测为准。
OOM 排查与 Arthas 的使用。先判断区域:Java heap space 是堆,GC overhead limit exceeded 多为泄漏,Metaspace 是类元数据过多,unable to create new native thread 是线程触顶,Direct buffer memory 是堆外内存。准备动作比事后补救更重要:加 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath,容器里让 -Xmx 与内存 limit 对齐,避免被 OOMKiller 直接杀掉而没有现场。分析链路是 jstat -gcutil 看各代增长与 GC 频率 → jmap -histo:live 看对象分布 → dump 后用 MAT 看支配树与 GC Roots 引用链,判断是「一次性加载太多」(集合未分页、批量查询、缓存无上限)还是「缓慢泄漏」(监听器未注销、ThreadLocal 未 remove、连接或流未关闭)。工具按场景选:MAT 做离线 dump 分析,async-profiler 出低开销火焰图,jstack 看线程栈。Arthas 常用命令要能报出来:dashboard 看整体、thread -n 3 找最忙线程、thread -b 定位阻塞其他线程的锁、jad 反编译确认线上代码版本、watch 与 trace 看入参与耗时、stack 看调用来源、heapdump 导堆、vmtool 查对象、redefine 热替换类。风险是 trace 这类命令在大流量方法上要加条件与次数限制,否则会把线上打崩。
Redis 为什么快、分布式锁与 Redisson 续期。快是多层叠加的结果:数据在内存里、避免磁盘 IO;单线程处理命令,没有锁竞争与线程切换,也保证命令原子性(6.0 后 IO 多线程只做读写与协议解析);epoll 事件循环让单线程能扛大量连接;数据结构与编码做了针对性优化(listpack、intset、跳表、渐进式 rehash);协议简单并支持 pipeline 与 Lua 减少往返。分布式锁的标准实现是 SET key value NX PX ttl,value 用 UUID 加线程标识,释放时用 Lua 先比对再删除以避免误删。Redisson 在此基础上封装了可重入、阻塞等待与看门狗:加锁成功后启动后台任务,默认每 10 秒(TTL 的三分之一)检查业务是否仍在执行,是就把过期时间续回 30 秒;客户端宕机则续期停止、锁到期自动释放。两个坑必须说:只有不显式指定 leaseTime 时才启用看门狗,指定固定租期就没有自动续期;主从切换或网络分区时锁仍可能失效,所以业务必须叠加幂等,锁只能降低冲突概率。
应用内存与 Redis 内存的区别,以及三层缓存架构。区别在归属、可见性、容量与失效方式:应用内存属于 JVM 堆,随实例扩缩、实例间不共享、受 GC 与 -Xmx 限制,本地命中零网络开销但一致性最难;Redis 是独立进程,按 maxmemory 与淘汰策略(LRU、LFU、TTL、随机)管理容量,所有实例共享同一份数据,代价是网络往返与序列化。设计原则是「数据库是唯一真相源,缓存只做加速」:读走「本地缓存(可选,秒级 TTL)→ Redis → 数据库」依次回源并回填,回填加随机过期时间避免雪崩,热点用互斥重建或逻辑过期防击穿,不存在的键用空值或布隆过滤器防穿透;写走 Cache-Aside,先更新数据库再删缓存,必要时延迟双删或订阅 binlog 异步失效。缺点要主动讲:删缓存失败与主从延迟造成短期不一致;多级缓存让排查变复杂;Redis 成为强依赖,挂了会打穿数据库,需要限流、熔断与本地兜底;无界缓存与不当淘汰会吃内存,要监控命中率、大 key 与内存水位。
B 树与 B+ 树、Kafka 高水位线。B 树每个节点既存键也存数据,查找可能在中间节点命中,但单节点容纳的键更少、树更高、路径长度不一致,范围查询要在层间跳跃;B+ 树把数据全放叶子层、非叶子层只存索引键,扇出更大、树更矮(三到四层撑起千万级数据),查询路径等长、性能稳定,叶子用双向链表串联让范围扫描变成顺序遍历。B+ 树的缺点要如实说:等值查询可能多走一层到叶子;非叶子冗余存键使索引略大;分裂合并逻辑更复杂;非覆盖索引查询仍要回表。Kafka 的高水位线(HW)是「已被 ISR 中所有副本复制成功、因而可被消费者读取」的偏移上界:Leader 维护 LEO(下一条写入位置),HW 取 ISR 中各副本 LEO 的最小值,消费者只能读到 HW 之前,这保证读到的消息不会因 Leader 切换而丢失,代价是消费者进度滞后。副本落后超过 replica.lag.time.max.ms 会被移出 ISR,HW 由剩余副本决定;Leader Epoch 解决重启后按 HW 截断导致的不一致,unclean.leader.election.enable 则决定是否让非 ISR 副本当选(用一致性换可用性,会丢消息)。
Agent 项目的记忆、上下文压缩与 LangGraph 编排。记忆通常分三层:短期记忆是当前会话历史,按 token 预算保留原文并做摘要;长期记忆是跨会话的用户画像与事实,抽取成结构化条目存库或向量库,带来源与时间戳以便更新与溯源。检索时机是关键,不能每轮全量召回,而靠意图或实体触发(命中相关场景或工具即将调用时才查对应记忆),召回后按相关度与时效重排,并标注记忆来自何时,避免把过期偏好当事实。上下文压缩的做法有滑动窗口丢弃最旧消息、分层摘要(留结论与决策,丢寒暄与冗余工具输出)、把大块工具返回裁成结构化字段、用向量检索按需回溯原文;压缩必须保留不可丢项——用户明确约束、任务目标、已完成与失败的动作,否则模型会重复劳动或违背要求。LangGraph 是显式状态图,节点是函数、边是流转条件,所以流程既有线性部分(预定义处理链)也有动态部分(条件边、循环与路由,由模型或规则决定下一步),是否动态取决于你把决策放在边还是节点内部;工程上要注意状态用 reducer 合并、checkpoint 与业务库的单一真相源(业务库为准,checkpoint 只存控制流)、以及给循环设步数与超时上限防止跑飞。