BIGO Java 开发(大数据)一面:慢查询、G1 与线程池
- 轮次
- 一面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 请进行一下自我介绍
- 介绍下简历上的项目
- MySQL 出现大量慢查询时,你会按照什么顺序排查?
- 联合索引设计时,如何判断字段的排列顺序?
- 为什么有时候明明建立了索引,数据库仍然选择全表扫描?
- 大数据量下执行深分页查询,如何降低数据库压力?
- InnoDB 中一次更新操作从执行到提交,大致会经历哪些过程?
- EXPLAIN 中哪些信息最值得重点关注,如何结合起来判断问题?
- G1 垃圾收集器如何管理堆内存,为什么适合大堆场景?
- 如果线上出现频繁 Full GC,你会如何定位根因?
- CMS 与 G1 在回收策略和停顿控制上有什么本质区别?
- Java 线程池中,任务提交后会按照什么规则分配执行?
- 为什么不建议直接使用无界队列或默认线程池?
- CompletableFuture 组合多个远程调用时,如何处理超时和异常传播?
《参考解析》
慢查询的排查顺序:先定范围,再看执行计划,最后怀疑资源。 第一步不要把「慢」当成语句问题——先看慢查询日志、数据库监控和应用调用链,确认慢查询是集中在某个接口还是某张表,是突发的还是持续的。第二步用 EXPLAIN(或 EXPLAIN ANALYZE)看执行计划,重点核对:是否全表扫描、实际扫描行数是否远大于返回行数、有没有用临时表、有没有额外排序、是否发生回表、估算行数与实际行数是否偏差很大。第三步才转向 SQL 写法层面:隐式类型转换、函数包裹索引列、前置 % 模糊匹配、超大分页。如果执行计划正常,就别急着加索引——要继续排查锁等待、磁盘 IO、连接池耗尽、Buffer Pool 命中率和网络传输量。顺序反了就会「看着慢就加索引」,既没解决问题又增加了写入成本。
联合索引的字段顺序,不是简单的「区分度从高到低」。 要综合查询条件、字段区分度、排序需求和范围条件判断,一般原则是:等值过滤列在前,多个等值列之间结合实际查询频率和区分度排序,范围条件列放在等值列之后(范围列之后的字段通常无法继续用于索引定位),如果查询带排序则要考虑索引顺序能否消除额外排序(ORDER BY 与索引顺序一致时可省掉 filesort)。还要牢记最左匹配原则——(a, b, c) 并不意味着只查 b、c 时也能用上完整索引。最后一条纪律:不能只为某一条 SQL 设计索引,要观察整体索引数量和写入成本,索引越多写入越慢、优化器选择也越容易出错。
有索引却走全表扫描的原因。 优化器的目标是估算成本最低,不是「有索引就用」。常见情形有:查询返回的数据比例很高,回表成本超过全表扫描;表本身很小,全表更快;统计信息过期导致优化器误判数据分布;索引列上用了函数(CAST(account_id AS CHAR) = '10001');发生隐式类型转换;前置 % 模糊匹配;查询条件选择性太差;以及同一个 SQL 模板因参数不同走了不同计划。处理方式要分情况:统计信息异常就更新统计信息,写法问题就改写成类型一致、不对索引列做运算;FORCE INDEX 只能在充分验证后作为临时手段,不能当常规解决方案。
深分页的优化方向。 LIMIT offset, size 在 offset 很大时仍要扫描并丢弃前面所有行,分页越靠后越贵。首选是改成基于游标/业务唯一键的范围分页:WHERE tenant_id = ? AND (event_time < ? OR (event_time = ? AND id < ?)) ORDER BY event_time DESC, id DESC LIMIT ?,把偏移量换成范围定位,索引可以直接落到起点。次选是延迟关联:先在覆盖索引上分页拿到主键,再回表取整行。此外还可以在业务上限翻页数,或对超过阈值的翻页改成条件筛选。
InnoDB 一次更新的完整过程。 执行器先通过索引定位到目标行(如果不在 Buffer Pool 就从磁盘读入),在 Buffer Pool 中修改数据页并写入相应 undo log(用于回滚和 MVCC 读旧版本),事务提交前还要写 redo log——采用两阶段提交:先写 redo log 的 prepare 状态,再写 binlog,最后把 redo log 置为 commit。这个顺序保证了崩溃恢复时 redo log 与 binlog 的一致性(否则主从或备份恢复会出现数据不一致)。脏页不立即刷盘,由后台线程按策略刷;这也是「提交很快但断电可能丢最后一小段」的边界所在——innodb_flush_log_at_trx_commit 决定提交时是否强制刷盘,是性能与安全性的取舍点。
EXPLAIN 该看什么。 优先级从高到低:type(从 system/const/eq_ref/ref/range/index/all 依次变差,出现 ALL 就要警惕)、key 与实际使用的索引、rows(预估扫描行数)与 filtered(过滤后剩余比例)、Extra 里的 Using filesort / Using temporary / Using index(覆盖索引)/ Using index condition。把这些结合起来判断:如果 type=range 但 rows 极大且 filtered 很低,说明扫描量还是太大;如果 Extra 出现 Using filesort 且数据量大,就要考虑让索引顺序匹配排序。再看 EXPLAIN ANALYZE 给出的实际行数与耗时,与预估对比能发现统计信息问题。注意 MySQL 8.0 的 EXPLAIN FORMAT=JSON 还能看到代价估算,排查「选了坏计划」时很有用。
G1 与 CMS 的差别,以及频繁 Full GC 怎么查。 G1 把堆划分成大量大小相等的 Region(不再物理分代),用可预测停顿模型(用户设定目标停顿时间,G1 据此选择回收收益最高的若干 Region,按收益优先收集),并通过 Remembered Set 处理跨 Region 引用、用并发标记维护对象存活信息。它适合大堆的原因在于:回收是增量的、停顿时间可控,不像 CMS 那样受限于整代扫描的碎片与并发失败(Concurrent Mode Failure 会退化成一次长停顿的串行 Full GC)。CMS 走的是标记—清除(不压缩),停顿短但会产生碎片;G1 是标记—整理式的区域回收,碎片问题小得多。频繁 Full GC 的定位顺序:先用 jstat -gcutil 看各代占用与 GC 频率、用 GC 日志确认是哪种回收触发;再看是不是内存泄漏(老年代在每次 Full GC 后仍持续走高)、晋升过快(年轻代太小/对象太大直接进老年代)、元空间或大对象分配(-XX:MetaspaceSize、Humongous 对象占用连续 Region)、以及显式 System.gc()。定位到根因才谈调参,盲目调 -Xmx 或换收集器通常只是把问题推后。
线程池的任务分配规则与「别用无界队列」。 提交任务后的顺序是:核心线程未满则新建核心线程 → 核心线程满则入队 → 队列满且未达最大线程数则新建非核心线程 → 都满则执行拒绝策略。这个顺序是理解一切线程池问题的关键:队列一旦无界,maximumPoolSize 就永远用不上,任务会无限堆积直到 OOM——这正是 Executors.newFixedThreadPool(无界 LinkedBlockingQueue)和 newCachedThreadPool(最大线程数 Integer.MAX_VALUE)被规约禁止的原因。正确做法是手动构造 ThreadPoolExecutor:有界队列 + 明确的拒绝策略(CallerRunsPolicy 形成天然反压,或自定义策略降级并打日志)+ 自定义 ThreadFactory 给线程命名(线上 dump 才找得到人)+ 监控 activeCount/queueSize/拒绝次数 + 优雅关闭(shutdown + awaitTermination,超时再 shutdownNow)。参数按任务类型定:CPU 密集型约等于核数 + 1,IO 密集型按「核数 × (1 + 等待/计算)」估算,最终以压测为准。
CompletableFuture 组合远程调用的超时与异常传播。 三个要点:① 每个远程调用都要单独设超时(orTimeout/completeOnTimeout,或让底层 HTTP 客户端带超时),否则组合起来的整体超时救不回已经卡住的子任务;② 组合时 thenCombine/allOf 默认不会因为一个失败就取消其他任务,需要显式传播——用 exceptionally/handle 做降级(返回默认值)或 whenComplete 记录并上抛,还要在失败时主动 cancel 其余任务以释放资源;③ allOf 的返回值是 CompletableFuture<Void>,需要自己从各个 future 里 join 取结果,而 join 抛的是未检查异常(CompletionException),异步回调里抛出的异常也会被包装进去,所以要统一在最外层解包并区分「业务失败」与「超时/中断」。另外注意默认线程池是 ForkJoinPool.commonPool(),IO 密集场景应传入自建线程池,避免把公共池占满。