岳阳某小公司二面:分表跨月查询、线程池与 Spring 单例 Bean
- 轮次
- 二面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
一、自我介绍与项目经历
- 先介绍你最近做过的项目或实习,重点讲你觉得做得比较深入、印象比较深的部分。
- 针对第二段实习,挑一个印象比较深的功能开发例子具体讲讲。
- 你刚才说这个数据的分表,具体来说是怎么做的?
- 你是按月去切分表是吧?
- 如果我查的数据涉及跨月的时候怎么办?
- 如果我要查的是十月二十八号到十一月五号这一段时间的数据呢,怎么办?
- 你的十月份数据和十一月份数据就在两个表里(确认理解)。
- 假设现在是一月份,我要查的数据时间范围是十一月二十八号到十二月五号,这个时候怎么查?
- 如果现在要做展示和分页,比如把明细数据放到页面上,涉及跨月的数据,要告诉别人总共多少条,还要一页页分页,怎么办?
- 有可能你现在查的这一页并不需要查两个表——我指定了一个范围,数据在页面上可能有几十页,我翻到其中一页的时候怎么办?
- 后续还是要滚动地做每月数据的移入和原表删除,对吧?这部分移入和删除在数据库层面怎么执行?
- 这里边有没有考虑后续再做一些什么样的优化?
二、Java 基础
- 讲讲你对 Java 集合框架的了解。
- 比如 List,我们常用有哪些 List,它们之间的差异是什么?
- Set 呢?
- 场景题:有一份本地文件,包含每个学生的名字、班级、英语/语文/数学三个科目成绩,数据量可能几万到几十万条。需要找出每个科目成绩前三名(后又说前十名)的学生信息。结合 Java 集合框架,你会怎么设计实现?把详细步骤讲一下。
- 数据读完之后存到了 List 里边,那要找三个科目的学生前十信息,这个怎么处理?
- 讲一下 Java 里的多线程开发,做多线程开发有哪些常用的类或 API?
- 你提到线程池,用线程池做多线程开发时,通常容易遇到什么样的问题?怎么解决?
- 加线程数会有什么样的风险或负面影响吗?
三、Spring / Spring Boot
- 在 Spring Boot 框架下,处理请求通常会通过注解来声明类或方法,讲一些常用的注解以及它们的核心参数。
- 以单体架构来说,接收请求参数一般有哪些注解可以用来接收?
- Spring 容器里的 Bean 实例默认是单例还是多例?
- 为什么默认用单例?
- 这有什么样的好处?
- 对于这些单例的 Bean 实例,我们在使用过程中有什么注意事项?
四、数据库
- 有两张表:班级表(ID、班级名称),学生表(ID、学生名字、性别、所属班级 ID)。写一个 SQL,一次性统计出每个班级的学生人数。
- 你这里统计人数用了 sum?(追问,应为 count)
- 两个表做 join 的时候用什么 join?left 和 right 的区别是什么?
- 在这个基础上进一步明确:找到人数最多的十个班级,这个 SQL 还可以怎么调整?
五、AI 应用
- 做 AI 应用时,底层都是使用大模型提供的 API 来和大模型对话。直接调用大模型 API 对话,和使用一些 AI 工具(如 Claude Code 等)的对话形式,它们的差异是什么?
- 那它为什么能够实现,比如我对话之后还能做一些任务呢?
- 给到大模型的输入内容叫提示词,提示词分为系统提示词和用户提示词,这两种提示词的差异是什么?对应的业务场景是什么样的?
《参考解析》
1. 按月分表后的跨月查询与分页。 按月分表(order_202610、order_202611)本质是水平拆分,跨月查询就得做「路由 + 归并」。第一步按时间范围算出涉及哪些月份的表,只查这些分表而不是全部;第二步对每张表单独执行带条件的查询,在应用层归并。分页是难点:limit offset 在每张分表上执行会拿到错误的页码区间,正确做法是①精确总数:对每个分表 count(*) 后求和(大表要额外维护计数表,否则每次 count 都很贵);②按页取数:如果排序键是时间且查询范围连续,可以先算出该页落在哪张表、表内偏移多少,只查一张表;跨两张表时取「前一张表的尾部 + 后一张表的头部」,用「每表各取 offset+limit 条再归并截取」的兜底策略。要注意 offset 很大时每张表都要扫大量行,所以列表页应该配合书签/游标分页(用 (created_at, id) < (?, ?)),而不是深 offset。
2. 滚动迁移与优化。 每月把热数据迁到历史表、再删原表数据,不能一条 delete ... where 干完:大事务会长时间持锁、撑爆 undo log、还会阻塞主从。工程做法是分批迁移——每批 500~2000 行,按主键区间推进,insert into history select ... where id between ? and ?,再按同一区间删除,每批提交并记录进度表(迁移到哪个 id),失败可从断点续跑;迁移期间双写或读时合并,保证数据不丢。后续优化方向:冷热分离(历史表换成更便宜的存储或列存)、给查询加覆盖索引减少回表、把「总数」从实时 count 改成计数表/近似值、以及按查询模式把「按月分表」升级成「按租户或业务键分片」,避免所有查询都要扫多表。
3. 集合框架与前十名。 集合分三条线:List(有序可重复,ArrayList 数组随机访问快、尾部追加均摊 O(1),LinkedList 双向链表插入删除快但随机访问 O(n),实际几乎都用 ArrayList)、Set(不重复,HashSet 基于 HashMap、O(1) 但无序,LinkedHashSet 保留插入序,TreeSet 红黑树有序 O(log n))、Map(HashMap 数组 + 链表/红黑树,LinkedHashMap 可做 LRU,TreeMap 有序,ConcurrentHashMap 线程安全)。找每科前十:不要对几十万条做全量排序(O(n log n)),用「小顶堆」维护每科大小为 10 的堆,遍历一遍 O(n log 10);三门课就是三个堆,或者一个 Map<String, PriorityQueue<Student>>。如果数据在文件里,可以流式逐行读、边读边压堆,内存只占 O(1);若要求同分时的稳定排序,堆的比较器里补上学号作为第二关键字。
4. 线程池的常见问题。 ① 队列无界(LinkedBlockingQueue 默认容量 Integer.MAX_VALUE)导致任务堆积、内存涨爆,应该用有界队列 + 明确的拒绝策略;② 核心线程数拍脑袋,CPU 密集按核数 +1、IO 密集按「核数 × (1 + 等待时间/计算时间)」估算,再用压测校准;③ 线程泄漏:任务里阻塞不返回,线程池被占满,需要设置超时和隔离(不同业务用不同池);④ 异常被吞:submit 返回的 Future 不 get 就看不到异常,execute 才会直接抛;⑤ 上下文丢失:ThreadLocal 在池化线程里残留,要在任务前后清理或用 TransmittableThreadLocal;⑥ 拒绝策略选错:CallerRunsPolicy 会反压到调用线程,AbortPolicy 直接抛异常,要按业务选。加线程数不是越多越好——线程多了上下文切换、锁竞争、内存占用都上升,还可能把下游打挂,正确做法是限流 + 扩容 + 异步化,而不是无限加线程。
5. Spring 单例 Bean 的注意事项。 默认单例是为了省对象创建和 GC 开销、让依赖注入和 AOP 代理只做一次,启动快、内存省。代价是所有请求共享同一个实例,所以:① Bean 里不能放请求级状态(用户 id、临时变量),要用方法局部变量或 ThreadLocal;② 有状态字段必须做线程安全(AtomicInteger、ConcurrentHashMap,或改原型 @Scope("prototype"));③ 单例里注入原型 Bean 只会注入一次,需要 ObjectProvider 或 @Lookup;④ 初始化耗时长的资源要懒加载或异步初始化,避免启动阻塞。常用注解:@RestController、@RequestMapping/@GetMapping、@RequestParam、@PathVariable、@RequestBody、@RequestHeader、@Valid,核心参数是 value/name、required、defaultValue。
6. 两表 join 与分组统计。 统计每个班级人数:
SELECT c.id, c.name, COUNT(s.id) AS student_count
FROM class c
LEFT JOIN student s ON s.class_id = c.id
GROUP BY c.id, c.name;
用 LEFT JOIN 是为了「没有学生的班级也要出现,人数为 0」;COUNT(s.id) 而不是 COUNT(*),否则空班级会被算成 1。sum 在这里是错的,sum(1) 只能统计有学生的行且语义不对。LEFT JOIN 保留左表全部行、右表不匹配补 NULL;RIGHT JOIN 反之;INNER JOIN 只保留两边都匹配的行。取人数最多的十个班级:
SELECT c.id, c.name, COUNT(s.id) AS cnt
FROM class c LEFT JOIN student s ON s.class_id = c.id
GROUP BY c.id, c.name
ORDER BY cnt DESC, c.id ASC
LIMIT 10;
数据量大时先子查询聚合再 join,或把计数维护在统计表里避免每次全表 group by。
7. 直接调 API 与用 AI 工具的区别。 直接调 API 是「一问一答」:你把完整上下文塞进请求,模型返回一段文本,工具调用、文件读写、多轮状态全靠你自己实现。Claude Code 这类工具在模型外面套了一层 Agent 运行时:它把系统提示词、工具定义(读文件、执行命令、搜索)、历史对话和工具结果拼成每一轮的上下文,模型输出「调用某个工具」的结构化指令,运行时执行后把结果回填,再进入下一轮,直到模型判断任务完成。差别在于:① 上下文管理(压缩、裁剪、按需读取)由运行时负责;② 工具有副作用,需要权限与确认;③ 有循环和预算控制,防止无限调用;④ 结果要落盘、可回滚。所以「对话之后还能做任务」不是模型自身记住了什么,而是外层循环在不断把环境反馈喂回模型。
8. 系统提示词与用户提示词。 系统提示词由应用方写定,定义角色、能力边界、输出格式、安全规则和工具使用方式,对整段会话持续生效,优先级高于用户输入;用户提示词是当次请求的具体任务和素材。业务上,系统提示词承担「产品人格 + 约束」(比如面试官角色、评分维度、不许泄露内部信息),用户提示词承担「这次要处理什么」(简历内容、岗位描述、追问)。工程上要注意:不要把可变业务数据写进系统提示词(否则无法缓存、也容易被注入影响),用户输入要做注入防护;两者拼接顺序和分隔标记要清晰,长会话里系统提示词要固定在开头以命中前缀缓存。