面试复习手册
Prepare · Understand · Practice
面试问答复习手册

从基础原理到项目实践,完整收录一面与二面题目。
选择左侧题目开始阅读,代码可一键复制。

2 DOCUMENTS / OFFLINE

mercury保险一面:面试题与参考回答

根据提供的截图整理,保留原题编号。截图只包含第 3~7 题,第 1、2 题未提供,不作补写。数据库回答以 MySQL / InnoDB 为主,算法使用 Java。回答为整理后的参考答案,不代表面试官标准答案。

3. SQL 索引怎么用?什么情况下索引会失效?

截图底部第 3 条:参考答案与补充

截图参考答案(原文,部分表述需结合下方说明理解):

or条件,like通配符,对索引使用函数,表达式,隐式转换(类型不匹配)(如果是MongoDB还可以加一个先sort再match导致索引失效)

面试时展开说:我会检查 OR 各分支是否有合适索引、LIKE 是否以通配符开头、索引列是否被函数或表达式包裹,以及参数类型是否导致列发生隐式转换。最后用 EXPLAIN 判断实际访问方式,不能把这些关键词一律等同于“索引失效”。

  • OR:某个分支缺少索引时可能扫描整表;多个分支都有合适索引时,也可能使用 Index Merge。参见 MySQL Index Merge 文档。
  • LIKE:普通 B+ 树索引对 LIKE 'abc%' 有机会做范围定位;LIKE '%abc%' 通常不能按固定前缀定位。是否扫描索引、是否高效是不同问题。
  • 函数、表达式和类型转换:尽量让被索引列保持原样,例如把 DATE(created_at)=某天改为当天起止范围;绑定参数与列类型匹配。函数索引等特殊设计需要另行判断。
  • MongoDB:不能说“先 $sort 后 $match 一定失效”。优化器可以把适用的 $match 移到 $sort 前,仍需查看优化后的 explain。参见 MongoDB 管道顺序优化。

下面保留索引设计、SQL 示例及更完整的追问说明。

面试回答:

索引的作用是减少查询需要扫描的数据,也可能帮助排序、分组,或者通过覆盖索引减少回表。设计索引时,我会先看高频 SQL 的过滤条件、关联条件、排序和返回字段,再结合数据分布设计联合索引,而不是给所有字段都建索引。

普通 InnoDB 索引通常按 B+ 树组织,联合索引按照字段顺序排序。比如索引 (merchant_id, status, created_at),适合商户和状态等值过滤后,再按创建时间做范围查询。需要遵循最左前缀,但这是索引定义的顺序,不要求 WHERE 中的条件按相同顺序书写。MySQL 联合索引说明

所谓“索引失效”需要区分:条件无法利用索引定位、只能使用部分索引,以及优化器认为全表扫描更划算。最终要用执行计划确认,不能只背“出现某个关键字就失效”。

联合索引示例:

CREATE INDEX idx_merchant_status_time
ON pay_order (merchant_id, status, created_at);

SELECT id, created_at
FROM pay_order
WHERE merchant_id = 1001
  AND status = 1
  AND created_at >= '2026-09-01 00:00:00'
  AND created_at <  '2026-10-01 00:00:00'
ORDER BY created_at
LIMIT 100;

假设 id 是 InnoDB 主键,上述查询所需字段可以由该二级索引提供,有机会实现覆盖索引。增加索引也会增加写入维护和存储成本,要根据查询收益取舍。MySQL 索引优化说明

常见情况与准确表述:

情况 示例 面试中应如何解释
联合索引缺少最左列 索引 (a,b,c),只查 b=2 通常无法进行常规最左前缀定位;仍可能出现索引全扫描或特定条件下的跳跃扫描,不能说完全不会用索引
索引列上使用函数 DATE(created_at) = '2026-09-15' 对普通 created_at 索引,通常无法直接形成高效定位范围;可改成日期范围,或评估匹配表达式的函数索引
索引列参与运算 amount + 1 = 100 优先改写为 amount = 99,使被索引列保持原样
字符串列与数字比较 phone = 13800138000,phone 为 VARCHAR 可能因字符串列发生数值转换,无法按原字符串索引快速定位;参数应使用字符串类型
LIKE 以通配符开头 name LIKE '%张'、name LIKE '%张%' 普通 B+ 树索引通常无法按固定前缀做范围定位;LIKE '张%' 则可能使用范围扫描
OR 中存在无合适索引的分支 a=1 OR b=2,只有 a 有索引 可能选择全表扫描;两个分支都有合适索引时,也可能使用 Index Merge
条件命中比例过高 查询表中绝大多数行 优化器可能认为大量二级索引回表比扫描整表更贵
统计信息偏差 估算行数与实际行数差异很大 可能选错计划,需要检查统计信息和数据倾斜

范围扫描、LIKE 前缀匹配和跳跃扫描的适用条件参见 MySQL 范围优化。字符串列与数字比较的限制参见 MySQL 类型转换。OR 的多索引访问参见 MySQL Index Merge。

函数条件改写示例:

-- 对索引列做函数运算,普通索引通常难以直接用于范围定位。
SELECT id FROM pay_order
WHERE DATE(created_at) = '2026-09-15';

-- 改成左闭右开区间,避免遗漏带小数秒的记录。
SELECT id FROM pay_order
WHERE created_at >= '2026-09-15 00:00:00'
  AND created_at <  '2026-09-16 00:00:00';

Java 使用 JDBC / MyBatis 时,要保证绑定参数的类型与字段匹配,并统一业务时区和数据库时间口径。

容易被追问的点:

  • 范围条件之后的字段不等于“完全失效”。例如 (a,b,c) 上的 a=1 AND b>10 AND c=3,c 通常不能继续缩小该连续扫描区间,但仍可能通过索引条件下推参与过滤,减少回表。MySQL ICP
  • !=、NOT IN、IS NULL 不应一律判定为不走索引,要看实际条件、命中比例和执行计划。
  • 把 OR 改成 UNION / UNION ALL 需要保持结果语义,尤其要处理重复行;不能机械替换。
  • EXPLAIN 中 key 非空不代表查询高效,仍需看扫描行数、过滤效果和回表量。

截图中 MongoDB 扩展的纠正:

“先 $sort 再 $match 一定导致索引失效”不准确。MongoDB 优化器可将适用的 $match 前移到 $sort 前面,实际索引使用应看优化后的执行计划,而不是只看代码中的阶段顺序。MongoDB 聚合管道优化

4. SQL 较慢的原因如何排查?除了索引,还有什么方法提高效率?

截图底部第 4 条:参考答案与补充

截图参考答案(原文):

用explain排查,排查锁竞争,硬件配置,对大表分区,缓存结果,批量更新数据(hive的话还可以加一个数据倾斜的处理方法,加salt,小表join的时候广播,group by语句添加distribute by控制分布)

面试回答顺序:先用执行计划确认扫描、关联和排序成本,再查锁等待与 CPU、内存、磁盘 I/O;确认瓶颈后,选择分区裁剪、缓存、适量批量读写等方案。不是所有慢 SQL 都能靠加索引解决,也不是所有大表都需要分区。

  • 分区:查询条件能裁剪掉不相关分区时才可能减少扫描;跨全部分区查询不一定更快。
  • 缓存结果:适合高频重复查询,需要明确有效期、更新与失效规则。
  • 批量更新:减少数据库往返,控制每批记录数和事务长度,避免长时间持锁。
  • 资源配置:依据监控确认 CPU、内存、I/O 或连接瓶颈后再调整,而不是直接认定硬件不够。

Hive 数据倾斜:截图里的三个关键词怎么展开?

先定位:查看任务耗时和数据量,确认是否少数热点 key 让个别任务处理了远多于其他任务的数据。

  • 加 salt:将热点 key 拆成多个子 key 分摊处理。聚合场景先按“原 key+salt”局部聚合,再去掉 salt 按原 key 汇总;SUM 可累加局部和,AVG 需要汇总 sum 和 count,不能直接平均各组平均值。JOIN 场景还要让另一侧正确匹配盐值,不能两边随意加随机数。
  • 小表广播 / MapJoin:小表满足内存条件时,可用 MapJoin 减少大表 JOIN 的 Shuffle;需要关注加载后的内存开销与 JOIN 类型限制。参见 Hive JOIN 文档。
  • DISTRIBUTE BY:按指定表达式分发数据,本身不保证解决 GROUP BY 倾斜。如果仍按同一个热点 key 分发,热点仍然集中;通常需要配合拆分热点和两阶段聚合。参见 Hive DISTRIBUTE BY 文档。

下面保留完整排查清单、执行计划示例和其他优化方式。

面试回答:

我会先确认慢的是数据库执行,还是连接池排队、网络传输、结果映射或整个接口。然后结合慢查询日志和调用链拿到真实 SQL、参数、耗时和调用次数,用 EXPLAIN 看访问路径;必要时在可控环境用 EXPLAIN ANALYZE 对比估算行数和实际执行情况。同时排查锁等待、长事务、CPU、磁盘 I/O 和缓存命中情况。

优化也要对症处理:减少扫描行数和返回字段,优化关联和分页,缩短事务,减少 Java 端 N+1 查询,按场景采用适量批处理、缓存、预聚合和读写分离。只有确认单实例容量或吞吐成为瓶颈后,才进一步考虑分库分表。

建议按以下顺序排查:

步骤 看什么 要回答的问题
确认现场 SQL、绑定参数、表结构、数据量、发生时段、P95/P99 一直慢,还是偶发慢?所有参数慢,还是特定商户慢?
分解接口耗时 获取连接、SQL 执行、结果读取、对象映射、下游调用 时间究竟花在哪里?
分析执行计划 key、访问方式、估算扫描行数、过滤比例、连接顺序、排序 是否扫描过多、重复访问或关联结果膨胀?
对比实际执行 实际行数、循环次数、各节点耗时 估算是否失真,哪个节点放大了成本?
检查并发等待 行锁、元数据锁、长事务、连接池队列 SQL 本身快,是否主要在等待?
检查资源 CPU、磁盘时延、Buffer Pool、临时表、并发量 是否资源饱和或热点集中?
验证改动 相同参数和负载下对比延迟、吞吐及资源消耗 是否真的改善,有没有伤害写入或其他查询?
EXPLAIN
SELECT id, amount
FROM pay_order
WHERE merchant_id = 1001 AND status = 1;

-- 会实际执行查询,应在已评估负载的环境中使用。
EXPLAIN ANALYZE
SELECT id, amount
FROM pay_order
WHERE merchant_id = 1001 AND status = 1;

EXPLAIN ANALYZE 会真正运行语句并返回实际执行信息,不能把它当成无执行成本的静态分析。MySQL EXPLAIN

索引之外的优化方式:

方法 适用场景 需要注意
只查需要的列、限制结果量 大字段、多列、一次返回太多数据 减少数据库读取、网络和 Java 对象分配
优化 JOIN 和子查询 一对多关联后数据成倍增长 先明确关联基数,减少无意义的中间结果
游标分页 深分页列表 需要稳定排序键;不适合直接任意跳到第 N 页
批量查询和适量批量写入 循环查库、逐条网络往返 控制批次,避免超大 IN、长事务和长时间持锁
缩短事务 锁竞争和长事务 事务内避免远程调用、用户等待等慢操作
缓存 高频、重复且允许一定延迟的查询 定义失效规则,并处理穿透、击穿和一致性
预聚合、汇总表 仪表盘反复统计海量明细 明确更新周期,处理补数和重复计算
读写分离 读压力大且允许复制延迟 强一致查询和写后立即读不能随意读副本
冷热分离、归档 历史数据大,热点集中在近期 保证历史查询和恢复路径可用
分区或分库分表 维护成本、容量或吞吐达到瓶颈 分区要能裁剪;分表要能精确路由,否则收益有限
调整资源和配置 明确存在 I/O、内存等瓶颈 依据测量调整,避免只靠扩容掩盖低效 SQL

深分页示例:

-- 偏移量很大时,通常仍需处理大量前置记录。
SELECT id, created_at
FROM pay_order
ORDER BY created_at, id
LIMIT 1000000, 20;

-- 用上一页最后一条记录的时间和 ID 继续向后查。
SELECT id, created_at
FROM pay_order
WHERE created_at > :last_time
   OR (created_at = :last_time AND id > :last_id)
ORDER BY created_at, id
LIMIT 20;

游标分页仍需要匹配排序和过滤的索引,并用执行计划验证。额外的唯一 ID 用于解决时间相同的记录排序问题;并发修改下若要求严格一致的分页,还需要快照或固定查询边界。

关于截图中的 Hive 扩展:

如果岗位确实涉及 Hive,可以补充分区裁剪、减少 Shuffle、预聚合以及排查热点 key。加盐需要配套正确的二次聚合或 JOIN 处理;小表广播要求小表能装入内存。单纯增加 DISTRIBUTE BY 不能保证解决同一个热点 key 的倾斜。Java/MySQL 面试中应先回答上述主线,再根据追问展开大数据引擎。

5. A、B 已落入 15 日数据,C 只落入 14 日数据,如何保证展示包含 ABC?如何与上下游沟通?

截图底部第 5 条:参考答案与补充

截图参考答案(原文):

取一个区间的时间数据再聚合,如果一个区间内数据都不存在和上游反馈产出时间问题

这句话怎么理解:如果业务允许按周、按月或某个日期区间展示,可以统一 ABC 的业务日期范围再分别聚合,避免只看最新一天时某类数据还未到齐。发现区间缺失时,向上游核实任务产出时间、完成状态、调度延迟和补数计划。

适用条件:扩大区间不会自动补齐缺失数据。如果选 1—15 日,A、B 已完整到 15 日,C 只完整到 14 日,那么 C 的区间结果仍不完整。不能只因为 ABC 在区间里“都至少有一条记录”就宣布数据齐全。

假设 ABC 的 1—14 日数据都完整:
同口径区间统计:A、B、C 都统计 1—14 日。
页面标注:完整数据截至 14 日,15 日 C 待更新。

如果必须展示 1—15 日:
C 标记“待更新 / 区间不完整”,不要把缺失当作 0。
15 日全部到齐并校验后,再统一发布完整区间结果。
  • 检查完整性:按业务日期、数据类型及批次检查完成状态,不能只看有没有记录;正常零业务量和未产出要区分。
  • 聚合口径:订单金额、笔数等可加指标可以按统一区间汇总;每日余额等快照指标通常不能直接跨天相加,需要另定期末值或平均值口径。
  • 与上游:确认业务日期、预计到数时间、完成标志、异常原因和补数时间;关键日期未完成或超时就沟通,不必等整个区间都没有数据。
  • 与下游:确认优先完整性还是时效性,解释当前统计区间、缺失类型和更新计划;未经约定不要混合不同日期的数据计算合计或比例。

下面保留“最近完整日、各类最新、指定日期部分结果”三种模式,以及就绪状态表和发布流程。

题目整理:

上游数据落入过程正常,但各任务调度时间不同。A、B、C 三类数据每天产出,目前 A、B 已经有 15 日数据,C 只有 14 日数据。如何设计数据展示,使 ABC 都被合理呈现,并与上游数据负责人、下游查询方协商?

面试回答:

我会先确认业务要的是“同一天完整的 ABC”,还是“每类数据各自最新”。这是两种不同口径。

如果页面需要横向比较、求合计或计算比例,我会默认展示 ABC 都已完成的最近业务日期。题目中如果三类的 14 日数据都完整,就统一展示 14 日,并明确标注“完整数据截至 14 日,15 日 C 待更新”。等 15 日 ABC 全部到齐并校验成功后,再统一发布 15 日版本。

如果下游更重视时效性,可以展示 A、B 的 15 日数据和 C 的 14 日数据,但每类必须标明业务日期与更新时间,避免让用户误以为来自同一天。另一种方式是展示 15 日页面,把 C 明确标为“待更新”,不能当作零,也不能无提示地拿 14 日数据填充。

三种展示方式:

模式 A B C 适用场景
最近完整日,默认推荐 14 日 14 日 14 日 同口径报表、比较、总额和比例计算
各类最新 15 日 15 日 14 日,突出日期 各指标独立,优先时效性
指定日期的部分结果 15 日 15 日 待更新 用户明确要求查看 15 日进度

金额、件数等实际值取自业务数据,这里只展示日期口径。不同业务日期的数据不能不加说明地合计。

后端如何落地:

维护一张数据就绪状态表,每个业务日期、数据类型和发布版本都有状态。例如:

dataset_status
  biz_date          业务日期
  dataset_type      A / B / C
  release_version   统一发布版本
  status            LOADING / COMPLETE / FAILED
  row_count         实际记录数
  completed_at      完成时间

唯一键:(biz_date, dataset_type, release_version)

COMPLETE 必须表示数据已完整可见并通过校验,不能只因为插入了第一条记录就标记完成。正常的零条数据也可以是 COMPLETE,要与“尚未到数”区分开。

查找各版本中 ABC 都完成的最近日期,可以用下面的示意 SQL:

SELECT biz_date, release_version
FROM dataset_status
WHERE dataset_type IN ('A', 'B', 'C')
  AND status = 'COMPLETE'
GROUP BY biz_date, release_version
HAVING COUNT(DISTINCT dataset_type) = 3
ORDER BY biz_date DESC, release_version DESC
LIMIT 1;

如果 A、B、C 使用各自独立的源版本号,则另建发布清单,将一个统一发布版本映射到三类源数据版本,而不是强行要求源版本号相同。

发布流程:

  1. 上游按业务日期和批次写入暂存区或不可变版本数据。
  2. 对记录数、必要字段、重复记录及关键业务总量进行校验,完成后标记该类数据 COMPLETE。
  3. 发布任务确认同一天 ABC 都完成,构建该版本展示结果。
  4. 数据准备完整后,原子切换“当前发布日期+版本”的指针。
  5. 一次页面请求固定使用同一发布版本,缓存键也包含版本,避免多个查询混用新旧数据。
  6. 迟到数据或历史修正生成新版本,校验后重新发布,保留可追踪的版本记录。

这是一种建议的系统设计,不依赖必须使用某一种调度平台。若上游支持任务依赖,优先让展示汇总任务依赖 ABC 的完成事件,不只依靠固定时间轮询。

为什么不能直接取三个 MAX 日期的最小值?

只有在每类数据按天连续完整产出、没有日期空洞时,min(max_date_A, max_date_B, max_date_C) 才能作为共同完成日期的简化算法。如果 A 有 13 日和 15 日,却缺少 14 日,这种算法可能错误地选中 14 日。

更可靠的方式是取三类“已完成业务日期集合”的交集,再选最近日期。若展示的是累计区间,还要检查区间内每天都完整,不能只检查结束日期。

与上游沟通:

  • 统一业务日期、时区、统计截止点,以及增量数据还是全量快照。
  • 约定每类数据最晚产出时间和完成信号,了解正常调度差异。
  • 约定完整性校验方式,区分“当天业务为零”和“数据未到”。
  • 明确迟到、失败、重跑、补数和数据修正的处理机制,以及超时告警和负责人。

可以这样说:“展示端需要同一天完整的 ABC。请提供每类数据的业务日期、完成状态和最晚到数时间;我们在三类完成后发布,超过约定时间则保留上一完整版本并告警。”

与下游沟通:

  • 确认完整性和时效性的优先级,默认展示哪个口径。
  • 确认可接受的延迟、缺失标识和页面提示。
  • 确认是否允许跨日展示,以及哪些合计、比例必须禁止跨日计算。
  • 约定接口返回 bizDate、releaseVersion、complete 和各类更新时间,便于解释与追溯。

可以这样说:“当前 15 日 A、B 已完成,C 尚未完成。默认报表展示完整的 14 日数据;如果需要查看最新进度,可以切换到 15 日,并明确标记 C 待更新。”

对截图参考答案的补充:

“取一段时间的数据再聚合”只在业务本来就需要区间统计时成立。它不能自动解决同日数据不完整,也可能把缺失问题隐藏在汇总中。区间聚合仍需要统一业务日期范围、定义完成条件并检查缺失。

6. LeetCode 206:反转链表

题目:

给定一个单链表的头节点 head,反转链表,返回反转后的头节点。假设输入为无环单链表。

输入:1 → 2 → 3 → 4 → 5 → null
输出:5 → 4 → 3 → 2 → 1 → null

面试回答:

使用迭代法,维护 prev 和 curr。遍历时先保存当前节点的原后继,再把当前节点的 next 指向 prev,最后推进两个指针。遍历结束后,prev 就是新头节点。关键是先保存 next,避免断开链表后丢失剩余节点。

Java 实现,推荐迭代:

class ListNode {
    int val;
    ListNode next;

    ListNode(int val) {
        this.val = val;
    }
}

class ReverseListSolution {
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode curr = head;

        while (curr != null) {
            ListNode next = curr.next; // 先保存原后继
            curr.next = prev;          // 反转当前指针
            prev = curr;               // 推进已反转部分
            curr = next;               // 继续处理剩余部分
        }

        return prev;
    }
}

复杂度: 时间 O(n),额外空间 O(1)。直接修改原链表,不创建新节点。

递归写法,可用于追问:

class RecursiveReverseListSolution {
    public ListNode reverseList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }

        ListNode newHead = reverseList(head.next);
        head.next.next = head;
        head.next = null; // 断开原方向,避免形成环
        return newHead;
    }
}

递归时间 O(n),调用栈空间 O(n)。长链表可能栈溢出,不能把递归解法说成 O(1) 空间。

需要覆盖的边界: 空链表返回 null;单节点返回原节点;双节点正确交换;反转后原头节点的 next 为 null;反转两次恢复原连接顺序。

7. LeetCode LCR 144:翻转二叉树

题目:

给定二叉树的根节点 root,将整棵树左右镜像翻转,返回根节点。

翻转前:          翻转后:
    4                4
   / \              / \
  2   7            7   2
 / \ / \          / \ / \
1  3 6  9        9  6 3  1

面试回答:

对每个节点交换左右子树,再递归翻转它的两个子树。遇到空节点返回 null。每个节点只处理一次,时间 O(n),递归栈空间 O(h),h 是树高;平衡树时 O(log n),最坏退化成链时 O(n)。

Java 实现,递归:

class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;

    TreeNode(int val) {
        this.val = val;
    }
}

class MirrorTreeSolution {
    public TreeNode mirrorTree(TreeNode root) {
        if (root == null) {
            return null;
        }

        TreeNode temp = root.left;
        root.left = root.right;
        root.right = temp;

        mirrorTree(root.left);
        mirrorTree(root.right);
        return root;
    }
}

Java 实现,迭代 BFS:

class IterativeMirrorTreeSolution {
    public TreeNode mirrorTree(TreeNode root) {
        if (root == null) {
            return null;
        }

        java.util.Deque<TreeNode> queue = new java.util.ArrayDeque<>();
        queue.offer(root);

        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();

            TreeNode temp = node.left;
            node.left = node.right;
            node.right = temp;

            if (node.left != null) {
                queue.offer(node.left);
            }
            if (node.right != null) {
                queue.offer(node.right);
            }
        }

        return root;
    }
}

迭代时间 O(n),队列空间 O(w),w 为最大层宽,最坏 O(n)。ArrayDeque 不允许 null,因此入队前要判空。

容易出错的地方:

  • 只交换根节点的两个孩子,没有继续处理每个子树。
  • 先覆盖左子树引用,再拿它给右子树赋值,导致原子树丢失;需要临时变量。
  • 把翻转二叉树理解成交换节点值;题目需要改变左右子树连接关系。
  • 忽略退化树上的递归栈深度。

需要覆盖的边界: 空树、单节点、只有左孩子、只有右孩子、不对称树,以及翻转两次恢复原树结构。

面试前快速复习

题号 记忆重点
3 围绕 SQL 设计索引;区分无法定位、部分使用和成本选择;用执行计划验证,避免绝对化结论
4 先定位耗时,再看执行计划、锁等待和资源;减少数据访问、网络往返与事务持锁时间
5 先确认业务口径;共同完成日期、就绪状态、统一版本发布、缺失提示和上下游 SLA
6 保存 next → 反转指针 → 推进 prev/curr;迭代 O(n) 时间、O(1) 空间
7 每个节点交换左右子树;递归 O(n) 时间、O(h) 栈空间;BFS 用队列

mercury保险二面:面试题与参考回答

按截图中的“二面 1、二面 2、二面 3”分组整理,保留原题顺序。每题包含可口述的参考回答、展开要点或代码。数据库以 MySQL / InnoDB 为主,Java 集合讨论以现代 JDK 常见实现为背景。

个人经历、项目技术栈和收益数据均需替换为自己的真实信息;下文的项目方案与话术是参考模板,不代表已经实施过。截图中“断电传输”按“断电或断网后恢复,即断点续传”理解。

二面 1:Java、Spring、数据库与算法

1. Spring 的 Bean 生成方式有哪些?

面试回答:

如果问的是如何把对象注册为 Spring Bean,常见方式有组件扫描、Java 配置类中的 @Bean、XML 的 <bean>、@Import,以及通过 BeanDefinition 动态注册。复杂对象的创建可以使用 FactoryBean。

如果问的是实例如何创建,容器可以通过构造器、静态工厂方法或实例工厂方法实例化对象。注册 Bean 定义与对象实例化是两个层次;依赖注入则是把依赖装配给已由容器管理的对象。

方式 示例或用途
组件扫描 @Component、@Service、@Repository、@Controller,用于自己编写的业务类
Java 配置 @Configuration 中声明 @Bean,适合第三方组件或需要显式创建逻辑的对象
XML <bean class="...">,常见于存量项目;也可指定工厂方法
@Import 导入配置类或组件;结合 ImportSelector、ImportBeanDefinitionRegistrar 做条件或动态注册
FactoryBean<T> 容器通过工厂获取目标对象,适合复杂创建过程
编程注册 向 BeanDefinitionRegistry 注册定义,适合框架扩展和动态装配

@Bean 方法描述容器管理对象的创建方式,返回的对象可以通过构造器或第三方工厂创建。Spring @Bean 官方说明

常见追问:

  • @Autowired 和 @Resource 主要用于依赖注入,不等同于注册一个新的 Bean。
  • 普通业务代码里直接 new 的对象,不会仅因为类上有注解就自动受到 Spring 管理。
  • 默认 singleton 是“每个容器、每个 Bean 定义一个共享实例”,不意味着整个 JVM 中该类只能有一个对象,也不自动保证线程安全。
  • 常见生命周期可概括为:实例化 → 属性注入 → 初始化回调及后处理 → 使用 → 容器关闭时销毁。具体扩展点允许提前代理等特殊处理,这不是所有 Bean 都完全相同的固定调用清单。
  • FactoryBean 是创建目标对象的工厂扩展点;BeanFactory 是 IoC 容器接口。按名称获取 FactoryBean 定义的 Bean 时,通常得到它的产品;使用 &beanName 获取工厂本身。Spring 容器扩展点

2. AOP 编程在 Spring 中如何体现?有什么作用?

面试回答:

AOP 把事务、日志、监控等横切逻辑从业务方法里抽出来,在匹配的方法调用前后统一执行。Spring AOP 主要通过运行时代理实现:调用先进入代理,再执行拦截器链和目标方法。

实际项目中,@Transactional 的声明式事务就是典型应用;也可以用 @Aspect 定义切面,对指定方法做耗时统计或审计。这样能减少重复代码,但必须理解代理边界,否则注解可能没有效果。

概念 含义
Aspect,切面 一组横切逻辑及其应用规则
Join Point,连接点 在 Spring AOP 中主要是方法执行
Pointcut,切点 选择需要增强的方法
Advice,通知 Before、AfterReturning、AfterThrowing、After、Around 等增强逻辑
Target / Proxy 被增强对象,以及调用者经过的代理对象

以上概念与通知类型参见 Spring AOP 官方说明。

代理方式与失效边界:

  • JDK 动态代理基于接口;CGLIB 基于生成目标类子类。实际采用哪种还受框架和项目配置影响,不能仅凭“有没有接口”猜测所有 Spring Boot 项目的行为。
  • CGLIB 不能通过重写增强 final 方法,也不能继承 final 类;private 方法不能靠这种代理方式增强。
  • 同一个对象内部 this.method() 的自调用绕过代理,目标方法上的事务或切面通常不会重新触发。
  • 常见处理是把需要增强的方法拆到另一个 Bean,通过注入对象调用;不要优先依赖获取当前代理等侵入性做法。
  • @Aspect 风格并不意味着已经使用 AspectJ 编译期或加载期织入;Spring AOP 与完整 AspectJ 织入需要区分。Spring 代理机制

举例话术:“统计接口耗时时,环绕通知中记录开始时间,执行 proceed(),在 finally 中记录耗时;异常继续向上抛,不能为了记录日志吞掉业务异常。记录参数时也要避免输出密码、令牌等敏感字段。”

3. 了解哪些 Java 数据结构?

面试回答:

我会从数组、链表、哈希表、树、堆和队列讲起,再结合 Java 集合的实现和使用场景。选型主要看是否需要按下标访问、保持顺序、去重、范围查询,以及是否存在并发修改。

结构 / 常用集合 主要特点 典型复杂度与适用场景
数组、ArrayList 连续的数组槽位;ArrayList 可扩容 下标访问 O(1),尾部添加摊还 O(1),中间插删通常 O(n);适合列表和遍历
LinkedList 双向链表,同时实现 Deque 按位置查找 O(n),端点操作 O(1);不能笼统说任何插删都比 ArrayList 快
HashMap 哈希桶,现代实现冲突时可使用链表或树结构 哈希分布良好时查找、插入平均 O(1);适合键值查找
HashSet 基于哈希结构去重,常见实现复用 HashMap 平均 O(1) 的成员判断;元素必须正确实现 equals/hashCode
LinkedHashMap 在哈希查找基础上维护迭代顺序 可按插入或访问顺序组织;能作为简单 LRU 的基础
TreeMap / TreeSet 红黑树,有序 查找、插入、删除 O(log n);适合有序和范围访问
PriorityQueue 二叉堆 查看堆顶 O(1),插入、弹出 O(log n);遍历不保证全局有序
ArrayDeque 可扩容数组实现的双端队列 常见端点操作摊还 O(1);适合栈、队列、BFS,不接收 null
ConcurrentHashMap 并发键值容器 支持安全并发访问;复合操作要用 compute、merge、putIfAbsent 等适合的接口
BlockingQueue 带等待机制的队列接口 适合生产者消费者;需选择容量和阻塞策略

集合分类与接口关系参见 Java 集合框架官方概览。具体性能边界要结合实现和数据分布,不能把平均复杂度当作无条件最坏复杂度。

常见追问:

  • HashMap 不是线程安全的;将它放进单例 Bean 不会自动变成线程安全。
  • Map 的 key 应保持影响 equals/hashCode 的字段稳定,否则放入后修改可能导致查找异常。
  • ConcurrentHashMap 不支持 null key 和 null value;“线程安全”不意味着跨多次独立调用的业务操作天然原子。
  • ArrayList 存储对象引用的数组槽位,对象本体不一定连续;遍历局部性通常仍优于逐个追踪链表节点。
  • 图可用邻接表或邻接矩阵表示,再用 BFS / DFS 遍历;Trie 适合前缀检索,但 Java 标准集合没有直接提供通用 Trie 类。

4. B+ 树相比二叉树有什么优点?

面试回答:

数据库索引的关键成本通常是访问数据页。B+ 树一个节点可以存放很多键和子节点引用,分支数远大于二叉树,同样的数据量下树高更低,查找通常需要更少的页访问。

另外,B+ 树的数据记录或记录引用集中在叶子层,内部节点专注导航,叶子节点按键有序并相互连接,因此范围扫描和顺序遍历很合适。

比较项 B+ 树 平衡二叉搜索树
分支数 多路,适合利用一个页的容量 每个节点最多两个孩子
高度 通常较低 相同节点规模下通常更高
范围访问 定位起点后沿叶子顺序扫描 需要通过树遍历访问后继
典型场景 磁盘或页式数据库索引 内存有序结构,如 TreeMap

假设有效分支数约为 100,一百万个叶子项所需导航层级会比二叉树少很多;这里只解释数量级,实际高度取决于页大小、键宽、记录大小和填充率,不能据此承诺固定 I/O 次数。

补充:普通二叉搜索树还可能退化成链表,但不能把所有二叉树都说成会退化,AVL 和红黑树会保持平衡。B+ 树也不是在所有内存场景都更优。InnoDB 聚簇索引叶子保存行数据,二级索引叶子保存索引列和主键值,二级索引查询可能需要回表。

5. 脏读、不可重复读、幻读怎么解决?

面试回答:

脏读是读到别的事务尚未提交的数据;不可重复读是同一事务内重复读取同一行,看到其他事务提交后的不同值;幻读是按相同条件读取一个集合时,看到其他事务导致的集合成员变化,典型情况是新增了满足条件的行。

解决方式要结合隔离级别和读取方式。在 InnoDB 中,RC 可以避免脏读;RR 下普通一致性读复用快照,避免因其他事务提交导致的重复查询变化。需要保护“当前满足某个条件的记录范围”时,必须考虑锁定读和范围锁,而不能只依赖快照。

隔离级别 脏读 不可重复读 幻读与使用边界
READ UNCOMMITTED 可能 可能 可能
READ COMMITTED 避免 可能 每次一致性读可看到新的已提交状态
REPEATABLE READ 避免 普通一致性读复用快照 InnoDB 的快照读与范围锁机制分别解决不同读取场景,不能笼统说所有操作都看见同一状态
SERIALIZABLE 避免 避免 提供更强隔离,需承担更多冲突、等待和重试

InnoDB 默认隔离级别是 RR,但生产环境可能被改为 RC,应查询实际配置。MySQL 隔离级别

RR 下要区分两种读:

  • 普通一致性读:通常是普通 SELECT。重复读取同一快照,不看见别的事务之后提交的新行;本事务自己的写入是例外,能够被本事务看到。
  • 当前读 / 锁定读:如 SELECT ... FOR UPDATE、UPDATE、DELETE。访问当前可操作的记录并加锁。在 RR 下,范围访问可使用 next-key lock 保护扫描范围;唯一索引等值命中现有记录时通常只需记录锁。MySQL 一致性读、MySQL 锁机制
-- 假设 user_id 有合适索引,且会话使用 RR。
START TRANSACTION;

SELECT id
FROM reservation
WHERE user_id = 1001
FOR UPDATE;

-- 在同一事务中完成依赖该查询结果的校验与修改。
COMMIT;

不能把普通 SELECT 的历史快照与 UPDATE 的当前状态混为一谈。是否阻塞插入、锁住多大范围,还取决于索引、执行计划和隔离级别。防止“重复申请”“重复订单”等规则时,优先使用数据库唯一约束兜底;这类业务并发问题不一定只靠提高隔离级别就能解决。

6. MySQL 并发读写是怎么实现的?

面试回答:

InnoDB 主要通过 MVCC 和锁配合实现并发。普通一致性读根据 Read View 判断版本可见性,必要时通过 undo 中的历史信息重建旧版本,所以读旧快照通常不必等待另一个事务修改当前记录。修改同一记录的事务则通过排他锁串行化,锁定读也会参与锁竞争。

几个关键部件:

部件 作用
事务 ID 标识记录版本对应的修改事务
undo 信息与版本链 支持回滚,也用于构建一致性读需要的旧版本
Read View 描述创建视图时哪些事务版本可见
记录锁、间隙锁、next-key lock 协调冲突修改,按场景保护记录或范围
redo log 用于崩溃恢复,不是读取历史快照的版本链
binlog 服务复制、CDC 等,不负责判断快照版本是否可见

多版本与 undo 的关系参见 InnoDB 多版本机制。

RC 与 RR 的区别:RC 通常每次一致性读建立新快照;RR 通常在第一次一致性读时建立并复用快照,并非普通 BEGIN 一执行就一定生成快照。START TRANSACTION WITH CONSISTENT SNAPSHOT 等用法另有规则。MySQL 一致性读

业务代码还要防止丢失更新。例如两个线程先读库存再各自写回计算结果,可能覆盖彼此的业务修改。可使用数据库条件更新:

UPDATE stock
SET available = available - :quantity,
    version = version + 1
WHERE sku_id = :sku_id
  AND available >= :quantity
  AND version = :expected_version;

调用方保证 quantity > 0,并检查影响行数。0 行代表库存不足、版本冲突或记录不存在,需要按业务区分;扣减、订单创建等还应有正确事务和幂等边界。若不需要检测版本变化,单纯防超卖可使用正数数量约束和 available >= :quantity 的原子更新。

追问重点:MVCC 不代表所有读写互不阻塞;同一热点行仍会争锁。长事务会延迟旧版本清理。死锁发生后应在业务幂等前提下有界重试整个事务,并采用一致的加锁顺序、缩短事务来降低概率。

7. MongoDB 相比 MySQL 有哪些优势?

面试回答:

MongoDB 的优势主要是文档模型、嵌套结构和内置分片能力。如果一份业务资料天然由一个主体和一组经常一起访问的子数据组成,文档嵌入可以让一次读取拿到整个聚合,减少应用层拼装;字段差异较大的资料类数据也更容易演进。

但我不会说 MongoDB 一定比 MySQL 快,也不会说它没有事务。选择主要取决于访问模式、数据关系和一致性要求。

维度 MongoDB MySQL
模型 BSON 文档,适合嵌套聚合 关系表,适合明确结构和关系
字段变化 灵活,可配合校验规则 通常通过表结构与约束控制,也有 JSON 类型
聚合读取 合理嵌入可一次读取相关数据 可通过 JOIN、查询模型或适当冗余实现
横向扩展 提供分片集群机制,仍需设计 shard key 可通过应用分片、中间件或其他分布式方案实现
事务与约束 支持单文档原子操作及多文档事务 多表事务、外键和关系约束体系成熟

嵌入模型的优势与限制参见 MongoDB 文档嵌入,分片及分布式事务能力参见 MongoDB 分片说明。

结合保险业务:结构变化较多的投保资料、调查材料元数据可以评估文档模型;账务、支付和保单核心关系数据通常优先评估关系模型。图片和视频本体更适合对象存储,数据库保存元数据。MongoDB 单文档有 16 MiB 大小限制,不能把无限增长的历史记录一直嵌入一个文档。

8.1 手撕 LeetCode 102:二叉树的层序遍历

题目:从上到下、每层从左到右返回二叉树节点值,结果按层分组。

输入:[3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]

思路:使用队列做 BFS。每轮开始先记录当前队列长度,这些节点恰好是当前层;处理它们时把子节点加入下一层。不能把循环条件直接写成动态变化的 queue.size()。

class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;

    TreeNode(int val) {
        this.val = val;
    }
}

class LevelOrderSolution {
    public java.util.List<java.util.List<Integer>> levelOrder(TreeNode root) {
        java.util.List<java.util.List<Integer>> result = new java.util.ArrayList<>();
        if (root == null) {
            return result;
        }

        java.util.Deque<TreeNode> queue = new java.util.ArrayDeque<>();
        queue.offer(root);

        while (!queue.isEmpty()) {
            int levelSize = queue.size();
            java.util.List<Integer> level = new java.util.ArrayList<>(levelSize);
            for (int i = 0; i < levelSize; i++) {
                TreeNode node = queue.poll();
                level.add(node.val);
                if (node.left != null) {
                    queue.offer(node.left);
                }
                if (node.right != null) {
                    queue.offer(node.right);
                }
            }
            result.add(level);
        }
        return result;
    }
}

复杂度:时间 O(n),不含返回结果的辅助空间 O(w),w 为最大层宽,最坏 O(n);返回结果本身 O(n)。边界包括空树、单节点、退化链和不完全二叉树。

8.2 手撕 LeetCode 31:下一个排列

题目:原地将整数数组改为字典序中紧邻的下一个更大排列。如果不存在更大排列,则改为最小排列。

[1,2,3] → [1,3,2]
[3,2,1] → [1,2,3]
[1,1,5] → [1,5,1]

面试思路:先从右向左找第一个 nums[i] < nums[i+1] 的位置,此时它右边是非递增后缀。再从右边找第一个比 nums[i] 大的值交换,让前缀只增加尽可能小的幅度。最后反转后缀,使后缀变为升序,也就是最小。若第一步找不到位置,整个数组已经最大,直接反转。

class NextPermutationSolution {
    public void nextPermutation(int[] nums) {
        if (nums == null || nums.length < 2) {
            return;
        }

        int i = nums.length - 2;
        while (i >= 0 && nums[i] >= nums[i + 1]) {
            i--;
        }

        if (i >= 0) {
            int j = nums.length - 1;
            while (nums[j] <= nums[i]) {
                j--;
            }
            swap(nums, i, j);
        }

        int left = i + 1;
        int right = nums.length - 1;
        while (left < right) {
            swap(nums, left++, right--);
        }
    }

    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

复杂度:时间 O(n),额外空间 O(1)。后缀本来有序,反转即可,不必排序。注意用 >= 和 <= 处理重复元素。

二面 2:数据迁移、故障处理与算法

1. 数据迁移的时候,怎么知道进度?

面试回答:

我会把迁移拆成结构准备、全量迁移、增量追平、数据校验和切换几个阶段,各阶段单独记录状态。全量阶段看成功落库的行数或字节数、已完成分片数、吞吐和失败数;增量阶段看源端位置、已持久化的目标端消费位置,以及延迟和积压,不能只看一个总百分比。

任务进度应以已经成功提交的数据为准,并持久化到任务表或框架检查点,避免应用重启后丢失。UI 可以通过轮询或 SSE 展示,展示方式不影响后台进度的可靠性。

任务记录示例:

migration_job
  job_id、source、target、phase、status、snapshot_boundary
  estimated_total_rows、committed_rows、failed_rows
  start_time、last_heartbeat、last_error

migration_task
  job_id、task_id、table_name、range_start、range_end
  last_committed_key、source_checkpoint、status、retry_count
  rows_committed、bytes_committed、lease_owner、lease_epoch
阶段 主要指标 解释
结构准备 已完成表数、索引或约束准备状态 明确哪些结构已就绪
全量复制 已提交行数 / 总行数,或已完成分片 / 总分片 总量若来自估计值,进度也应标为估算
增量追平 端到端延迟、队列积压、已应用源位置 读到事件不等于目标已提交
数据校验 已校验范围、差异行数、待修复范围 复制 100% 不代表正确性已验证
切换 写入隔离、最终位置、路由切换、验证状态 明确任务是否真正完成

关键细节:

  • 不要使用 当前最大主键 / 总最大主键 作为精确进度,主键可能有空洞、分布不均或不是数字。
  • 源库持续写入时,总行数会变化;需要固定快照边界或说明估算口径。
  • 剩余量 / 最近平均吞吐 可以给出 ETA,但索引构建和数据校验等阶段应单独估计。
  • 增量流没有永久固定的终点。切换时确定最终边界,等待目标应用完成该边界,才谈“追平”。
  • GTID 是事务集合标识,不能简单当作连续数字相减计算剩余事务数;时间延迟还需考虑时钟偏差和源端空闲,可配合心跳事件监控。

2. 用的什么技术栈?

回答原则:这是个人项目题,只说实际使用过的组件,说明职责、选型原因和你负责的部分。不要把下面所有工具都堆到一个项目里,也不要把参考方案说成自己的生产经历。

可替换的口述模板:

“项目使用 Java [实际版本] 和 Spring Boot,源端是 [实际数据库],目标端是 [实际数据库]。全量阶段用 [实际批处理工具] 按 [主键范围或其他方式] 分批迁移;如果需要在线迁移,通过 [实际 CDC 工具] 同步增量。任务状态保存在 [实际存储],监控使用 [实际工具]。我主要负责 [真实模块],重点处理了 [真实问题]。”

如果面试官让你现场设计,可给出两套参考选型:

场景 参考方案 重点
可安排停写窗口、数据规模适中 Spring Boot + Spring Batch / JDBC + MySQL 任务表 按范围拆任务,批量提交,持久化检查点,校验后切换
源端持续写入,需要全量加增量 Debezium 的一致性快照与 CDC + Kafka + 目标端消费程序 处理快照与 binlog 衔接、消费位点、重复事件和顺序

Debezium MySQL 连接器支持初始快照并衔接后续 binlog 变更;恢复时可能重发事件,因此目标端仍要处理幂等。具体快照和恢复行为要按所选版本与模式验证。Debezium MySQL 连接器

如果选 Spring Batch,需要使用持久化 JobRepository,并让 Reader / Writer 具备所需的可重启能力;框架存在不代表任意业务代码自动支持正确恢复。Spring Batch 分块处理

3. 怎么校验数据和表是否正确?

面试回答:

我会分为结构校验、数量校验、内容校验和业务校验。表结构要检查字段类型、精度、字符集、主键、唯一约束和索引;数据按固定主键范围或业务日期比较数量、关键字段和校验摘要;最后检查业务合计、关联关系和典型查询。在线迁移时必须对齐同一逻辑边界,否则源端持续变化会制造假差异。

校验层次 检查内容 容易漏掉的点
表结构 字段名、类型、长度、精度、nullable、默认值、索引、约束 signed/unsigned、DECIMAL 精度、时区、排序规则、自增后续值
数据数量 分表、分区、主键范围行数 总数相等仍可能一边多一条、另一边少一条
主键集合 缺失键、多余键、重复业务键 只查 COUNT 无法发现内容错位
数据内容 标准化后逐字段比较,或分块摘要后定位差异 null 与空串、字符编码、时间精度、JSON 顺序
业务约束 总金额、状态分布、关联记录、关键不变量 总金额相同也不能单独证明每笔都正确
读写能力 典型查询、执行计划、权限、写入和回滚 复制成功不等于应用可正常使用

落地步骤:

  1. 明确迁移字段映射与转换规则,统一比较口径。
  2. 按主键或其他稳定边界拆分校验任务,先比数量与关键业务汇总。
  3. 按确定顺序对记录做规范编码,计算分块校验值,避免含糊的字符串拼接。
  4. 对差异块继续拆分或逐行比较,生成缺失、额外、字段不一致清单。
  5. 修复后重新校验该范围,保留结果和可追踪记录。

哈希摘要用于快速筛查,不是数学上绝对无碰撞的证明。严格场景可以对关键记录逐字段比较。数据仍在变更时,可校验固定快照,或在切换窗口冻结写入、追平到最终位点后做最终核对;不能直接拿两个不同时间的库做全表对比。

4. 数据迁移失败了如何重跑?

面试回答:

核心是任务拆分、持久化检查点、目标写入幂等,以及保证旧任务不能覆盖新任务。失败后先确认错误类型,从最后成功提交的边界恢复,优先只重跑失败分片,不默认从头全量重跑。

一个批次的正确顺序:

读取一个批次
  → 转换与校验
  → 目标端写入并提交
  → 持久化本批次成功检查点
  → 读取下一个批次

如果写目标库与更新检查点能放在同一数据库事务中,就一起提交。若不能原子提交,应允许重复回放:先提交数据,后确认检查点;崩溃后最多重放一部分数据,通过幂等消除影响。反过来先确认进度再写数据,会在崩溃时漏数据。

幂等与恢复的细节:

  • 用源主键或明确的业务唯一键识别同一条记录,重复写入不增加重复数据。
  • upsert 只解决“重复插入”的一部分问题。存在乱序时,需要保证同一键的事件顺序,或者使用可靠的源版本 / 位点比较,防止旧数据覆盖新数据。
  • 删除事件也要同步;如有乱序或并行重放风险,可保留删除版本信息,避免旧插入把已删除记录复活。
  • 同一任务只允许一个有效执行者,租约到期重新领取时增加 fencing token / 执行世代,并在写入链路校验,防止失联旧进程继续提交。
  • 瞬时网络错误可指数退避并限制次数;字段映射或数据格式错误应修复后再跑,不能无限重试。
  • 错误记录可以隔离保存,但有未处理差异时不能直接宣布全量迁移成功。

CDC 恢复:保留源端和消费端持久化位点,并保证 binlog / 消息保留期覆盖最长恢复时间。如果所需源日志已经被清理,不能强行从最新位置继续并宣称没有丢数据,应重建相应快照或执行经过验证的补数流程。Debezium 恢复与快照说明

切换失败的追问:新库开始接受写入后,回滚必须处理新增数据和反向同步,不能只把连接地址改回旧库。是否可回滚、恢复哪份数据,应在切换前设计好。

5. 大盘业务出现问题时,你是怎么沟通的?

面试回答模板:

“我会先确认影响哪些指标、业务日期、用户和业务决策,区分数据延迟、数据错误和页面性能问题。然后建立统一的事件记录和负责人,同步已确认事实、暂时措施、当前排查方向和下一次更新时间。技术侧并行排查采集、调度、计算、存储和展示链路,业务侧明确当前哪些数据可用。恢复后做校验,再通知业务恢复使用,最后复盘根因和改进项。”

沟通内容应具体:

对象 主要信息
业务使用者 哪个页面或指标受影响、数据截至何时、是否可以继续决策、临时替代方案
上游负责人 具体数据集、业务日期、批次、缺失范围、完成状态、恢复预期
平台或运维 故障时间、链路证据、资源与日志、需要配合的动作
项目负责人 影响等级、资源需求、风险和下一次同步时间

可直接使用的情境示例,不代表真实经历:

“目前确认 15 日 C 数据尚未完成,A、B 正常,影响的是 15 日汇总页。页面已切换到完整的 14 日版本并标注日期;上游正在处理 C 任务,恢复时间还在确认。下一次在 [约定时间] 更新进展,数据到齐并校验后再发布。”

不要在未确认时给出精确恢复时间,也不要仅说“上游有问题”。如果展示错误数据,应显著标记、暂时关闭相关指标或回退到已验证版本,防止用户继续按错误数据决策。复盘形成负责人、期限和可验证的改进项,而不是只追责。

6. 手撕 LeetCode 304:二维区域和检索——矩阵不可变

题目:给定一个不再变化的矩阵,多次查询由 (row1,col1) 到 (row2,col2) 构成的闭区间矩形元素和。

面试思路:先建立二维前缀和。让 prefix[i+1][j+1] 表示原矩阵从 (0,0) 到 (i,j) 的区域和,额外增加一行一列零,统一边界处理。查询时用容斥:大矩形减上方、减左方,再加回重复减掉的左上角。

构建:
prefix[i+1][j+1] = matrix[i][j]
                  + prefix[i][j+1] + prefix[i+1][j]
                  - prefix[i][j]

查询:
sum = prefix[row2+1][col2+1]
      - prefix[row1][col2+1]
      - prefix[row2+1][col1]
      + prefix[row1][col1]
class NumMatrix {
    private final long[][] prefix;

    public NumMatrix(int[][] matrix) {
        // 题目保证非空矩形;这里同时检查输入形状。
        if (matrix == null || matrix.length == 0
                || matrix[0] == null || matrix[0].length == 0) {
            throw new IllegalArgumentException("matrix must be non-empty");
        }
        int rows = matrix.length;
        int cols = matrix[0].length;
        prefix = new long[rows + 1][cols + 1];

        for (int i = 0; i < rows; i++) {
            if (matrix[i] == null || matrix[i].length != cols) {
                throw new IllegalArgumentException("matrix must be rectangular");
            }
            for (int j = 0; j < cols; j++) {
                prefix[i + 1][j + 1] = (long) matrix[i][j]
                        + prefix[i][j + 1]
                        + prefix[i + 1][j]
                        - prefix[i][j];
            }
        }
    }

    public int sumRegion(int row1, int col1, int row2, int col2) {
        if (row1 < 0 || col1 < 0 || row2 < row1 || col2 < col1
                || row2 >= prefix.length - 1
                || col2 >= prefix[0].length - 1) {
            throw new IllegalArgumentException("invalid query bounds");
        }
        long sum = prefix[row2 + 1][col2 + 1]
                - prefix[row1][col2 + 1]
                - prefix[row2 + 1][col1]
                + prefix[row1][col1];
        return Math.toIntExact(sum);
    }
}

复杂度:初始化时间 O(mn),每次查询 O(1),额外空间 O(mn)。内部用 long 避免前缀累加发生 int 溢出,按题目接口返回 int;若查询结果超出 int 范围,上面代码会显式抛异常,生产接口可以直接返回 long。

边界与追问:检查单个元素、整张矩阵、单行、单列、负数以及触及首行首列的矩形。矩阵如果频繁更新,静态前缀和不合适,可以进一步讨论二维树状数组等动态结构。

二面 3:项目复盘、协作与文件上传设计

1. 最有成就的事情是什么?

回答方法:用 STAR 组织内容:背景、任务、行动、结果。重点说明你独立承担了什么、怎么做取舍,以及用什么证据判断成功。下面是填写模板,不能将占位内容直接当成真实经历。

口述模板:

“我比较有成就感的是 [真实项目中的具体改进]。当时 [用户或业务场景] 面临 [具体问题],例如查询延迟、迁移不可恢复或数据不完整,影响了 [真实后果]。

我负责 [明确职责]。首先通过 [日志、调用链、执行计划或数据核对] 确认瓶颈在 [真实根因],然后比较了 [方案 A] 和 [方案 B],结合 [成本、时效、一致性] 选择了 [实际方案]。实施中我完成了 [两三项关键技术动作],并通过 [压测、灰度、对账] 验证。

最后 [指标] 从 [真实基线] 改善到 [真实结果],同时 [正确性或稳定性证据]。对我来说,价值不只是解决一次问题,还形成了 [监控、自动校验、恢复流程或团队规范]。”

准备材料:能解释指标怎么测量、样本周期多长、个人贡献边界和剩余不足。没有可靠量化数据时,用真实可验证的结果,不要编造“提升 90%”等数字。

1.1 追问:如果不依靠业务沟通,纯技术上如何解决刚才的查询性能瓶颈?

面试回答:

我会保持既有业务语义和一致性要求,先用执行计划、实际扫描行数、调用链和资源指标确认瓶颈,再按影响成本从小到大优化。首先减少不必要的数据访问和回表、修正 JOIN 与分页;如果是反复执行的重聚合,再评估增量汇总或专用查询模型;如果是资源隔离问题,再考虑读副本、分析库或分片。

现象 技术方案 必须验证的代价
扫描远大于返回行数 调整联合索引、改写条件、覆盖查询 写入开销与索引大小
大量回表或宽行读取 精简字段、拆分大字段、调整读取路径 是否改变调用方需要的数据
深分页 稳定游标分页,或优化定位主键后取明细 是否必须支持任意跳页,排序是否稳定
重复聚合明细 增量汇总、预计算、查询结果复用 数据更新、补数、重复事件和一致性
事务查询和分析互相影响 资源隔离、独立查询存储或分析引擎 同步延迟、额外运维成本
热点读 合适的缓存与请求合并 缓存一致性、热点失效和返回时效
单实例容量或吞吐不足 分片、冷热分离、扩容 路由、跨分片查询和迁移成本

关键边界:“纯技术解决”不能偷偷把实时查询改成分钟级缓存,也不能删掉业务需要的关联或字段。增加异步链路是否可接受,必须服从题目已有约束。如果要求强一致,就选择满足该约束的优化路径并明确成本。详细排查方法可结合 一面第 4 题 复习。

2. 用三个词评价自己

可替换回答:

“我会用负责、严谨、善于协作这三个词。负责体现在 [发现问题后推进到验证关闭的真实例子];严谨体现在 [上线前做边界测试、数据校验或风险评估的真实例子];协作体现在 [与业务、测试或上下游共同解决问题的真实例子]。”

每个词最好用一句具体事实支撑。三个词应覆盖不同维度,不要只是“努力、勤奋、能吃苦”等近义词,也不要用“从不出错”这种无法证明的绝对描述。

3. 不加班时项目无法按期完成,你觉得是什么原因?

面试回答:

我会先拆解是计划和估算偏差、需求范围变化、依赖阻塞,还是可用人力与目标不匹配,而不是直接归因于个人效率。需要看最初承诺的范围、关键路径、实际完成速度和变更记录,找到剩余工作与交付时间的差距。

原因明确后,提前同步风险并给出选择:减少本期范围、分阶段交付、解决依赖、调整资源或调整时间。短期紧急情况下可以协商临时投入,但不能把持续加班当成默认容量,也不应通过省略关键测试来掩盖延期。

可能原因 如何验证 改进方式
估算遗漏 联调、测试、上线准备未计入工作量 分解任务,参考历史交付数据,预留风险缓冲
需求变化 范围增长但期限不变 建立变更影响评估,重排优先级
外部依赖 接口、数据、环境、审批迟迟不到位 前置确认,设置负责人和交付节点,能 mock 的先解耦
技术未知 难点到后期才暴露 提前验证关键路径和技术可行性
资源不匹配 关键成员被多项目占用、知识集中 明确可用容量,结对交接,减少并行任务
返工与质量问题 缺陷反复、验收口径不清 提前对齐验收标准,代码评审、持续集成与必要测试

加分表达:我会基于证据估算新的交付区间,并同步“每个选项会牺牲什么、保住什么”,让相关方能做决策,而不是到截止日前只报告做不完。

4.1 如何在后端实现断电后恢复传输,也就是断点续传?

面试回答:

大文件采用分片上传,后端创建上传会话,为文件分配 uploadId,并持久化分片完成信息。每片校验并可靠存储后才返回成功。客户端断电或断网恢复后,用 uploadId 查询已完成分片,只补传缺失部分。全部分片到齐后校验、合并,再把文件标记为可用。

生产上可以使用对象存储的 multipart upload 能力,Java 服务负责鉴权、签发受限上传凭证、维护业务状态和确认完成,避免大视频先穿过应用服务器再写存储。S3 分片上传流程

接口设计示例:

接口 作用
POST /uploads 提交名称、长度、文件摘要等,返回 uploadId 和分片计划
GET /uploads/{id} 查询服务端已确认的分片、状态和过期时间
PUT /uploads/{id}/parts/{partNo} 上传分片;直传存储时可改为申请该分片的上传凭证
POST /uploads/{id}/complete 校验并启动最终完成流程,支持幂等重试
DELETE /uploads/{id} 取消会话并清理未完成分片

接口路径是设计示例,不是某个云厂商 SDK 的原样 API。

持久化模型:

upload_session
  upload_id、owner_id、object_key、file_size、expected_hash
  chunk_size、total_parts、status、version、expire_at

upload_part
  upload_id、part_no、part_size、checksum、storage_etag、status
  UNIQUE(upload_id, part_no)

断电恢复时最重要的几个细节:

  1. uploadId 及本地文件身份要在客户端持久化。浏览器刷新后可能需要用户重新选择文件,并验证它仍是同一个文件。
  2. 服务端完成状态以可靠存储为准,不以“已进入 JVM 内存”或“前端进度条 100%”为准。
  3. 自建分片文件存储时,先写临时对象,校验并完成持久化,再发布分片记录;对象存储则在确认分片存储成功后记录结果。
  4. 对象写入与数据库更新通常不是一个事务。崩溃可能产生孤立分片,因此需要查询、对账或幂等重试来恢复,不能假设两者天然原子。
  5. 重复上传同一 partNo 且内容相同时返回已有结果;内容不同则拒绝冲突,不能静默覆盖后使最终摘要失配。
  6. 合并按 partNo 排序并检查数量、大小和摘要,用条件更新或受控任务保证只有一个有效的完成流程。
  7. 完成请求超时后,先查询最终对象和会话状态;最终对象可能已经生成,只是响应丢失,不能直接重新建一个文件。

状态机示例:

INIT → UPLOADING → VERIFYING → COMPLETED
                      ↓
                    FAILED

未完成会话还可进入 ABORTED / EXPIRED。

分片尺寸和并发数根据网络、对象存储约束及设备能力设置,例如从数 MiB 到数十 MiB 范围内压测选择,不固定认为越大或并发越高越好。会话需要校验归属和额度,过期分片要回收。

与 Range 的区别:HTTP Range 主要用于部分内容读取。上传续传应有明确的分片会话或服务端支持的偏移量协议,不是给普通上传接口随便加一个 Range 头就自动实现。

4.2 如何解决异地上传的网络限制?例如人在美国,数据库在中国

面试回答:

首先区分是带宽低、往返延迟高、丢包,还是目的服务根本不可达。文件本体应进入对象存储,数据库只保存业务元数据,客户端不直接连接中国数据库。

对于跨地域上传,可以让用户先上传到就近接入点或本地地域对象存储,再由服务端异步同步到最终地域;配合分片、断点续传和适量并发,把不稳定的长距离传输从用户一次请求中拆开。如果要求必须直接在中国完成存储,则评估可用的跨地域传输服务或网络连接,并用实测吞吐和成功率选择。

推荐讨论的架构:

美国用户
   → 就近上传服务 / 对象存储
   → 持久化上传任务
   → 跨地域传输工作进程
   → 中国对象存储
   → 目标文件校验完成后更新业务可用状态

中国 MySQL:保存文件 ID、目标对象地址、摘要、业务归属及状态。

中间地域只有在项目允许当地暂存数据时才采用。如果目标网络完全不可达,需要先解决可达路径;重试和增加线程都无法凭空打通网络。

问题 对策
RTT 高,小请求串行很多 复用连接、适量并行分片、减少逐片跨地域控制交互
丢包或网络抖动 分片重试、退避、续传、按实际吞吐调整并发
总带宽不足 排队传输、带宽调度、适合的网络服务;代码无法突破物理带宽上限
中心服务短时不可用 就近持久化后异步转发,任务可恢复
用户等待过久 返回任务 ID,区分接收完成和目标地域可用状态

容易踩的坑:

  • 普通 CDN 的下载缓存能力不等于支持上传加速,具体服务和跨地域链路能力要验证。
  • 视频一般已经压缩,再做通用压缩收益有限;转码压缩会改变原文件,不能无条件采用。
  • 不把海外接收成功标成“中国已落库完成”。建议区分 RECEIVED_LOCAL → REPLICATING → AVAILABLE。
  • 两地状态更新也需要幂等、重试和对账,不能仅靠一次 HTTP 回调保证最终一致。

4.3 如何校验上传的视频是否完整?

面试回答:

分两层检查:第一层验证传输后的字节与原文件一致,包括分片数、文件长度、分片摘要和整文件摘要;第二层验证它确实是结构有效、能按要求解码的视频。文件大小相同不代表内容相同,哈希相同也只能说明与所比较的源文件一致,不能证明源文件本身没有损坏。

建议校验流程:

  1. 初始化时记录预期文件长度、分片计划和整文件 SHA-256,明确摘要针对原始文件字节计算。
  2. 每片接收后核对大小与分片校验值,服务端实际计算或使用存储端验证能力,不能只保存客户端声称的摘要。
  3. 完成时检查分片编号无缺失、无重复,最后一片长度符合计划,并按顺序组合。
  4. 对最终对象按相同算法计算整文件摘要并比较,确认长度也一致。分片哈希简单拼接不等于整文件哈希。
  5. 使用媒体解析工具读取容器、音视频轨道、时长、编码和分辨率,再按业务要求做全量解码检查或抽检。
  6. 全部校验通过后标记可用;失败保留原因,并允许补传或重新上传。

对象存储的 ETag 不一定是整文件 MD5,尤其是分片上传场景,必须按服务提供的校验算法和 full-object / composite 语义使用。S3 上传完整性校验

媒体信息检查示例:

ffprobe -v error -show_format -show_streams -of json input.mp4

ffprobe 可以检查媒体格式与流信息,但成功读取元数据不代表每一帧都可解码。更严格场景应执行受控的全量解码校验;抽取首、中、尾片段只能降低成本,不能作为全文件无损坏的证明。FFprobe 官方文档

后端实现补充:

  • Java 可用 MessageDigest 进行流式 SHA-256 计算,配合固定大小缓冲区读取,避免整个视频载入内存。
  • 校验文件使用受控对象地址或本地路径;调用外部媒体工具时按参数列表执行,并限制时间和资源。
  • 如果上传后要转码,应先验证原文件完整性,再分别保存原文件摘要和派生文件摘要;不能拿转码后的字节与原文件哈希直接比较。
  • 恶意客户端可以对任意文件计算正确摘要,因此摘要只解决字节一致性;文件类型、权限和业务真实性是另外的问题。

面试前快速复习

题目 关键词
Bean 注册、实例化、依赖注入三个层次;组件扫描、@Bean、@Import、FactoryBean
AOP 代理、通知链、事务、自调用边界、final/private 限制
集合 访问方式、顺序、复杂度、并发要求;避免只背类名
B+ 树 高分支、低树高、页访问、叶子范围扫描
隔离与并发 RC/RR、Read View、undo、锁定读、next-key lock、条件更新
MongoDB 文档聚合、字段演进、分片能力;结合关系和事务要求选型
迁移进度 分阶段、已提交进度、检查点、增量延迟、最终切换边界
校验与重跑 同一数据边界、结构/数量/内容/业务校验、幂等、乱序、旧执行者隔离
行为题 真实证据、个人职责、取舍、结果、风险提前沟通
文件上传 uploadId、分片持久化、续传、幂等完成、异地转发、整文件校验
LC 102 BFS 队列,每轮固定当前层长度;O(n) 时间
LC 31 从右找上升点、交换最小更大值、反转后缀;O(n) 时间、O(1) 空间
LC 304 二维前缀和与容斥;初始化 O(mn),查询 O(1)
已读至文档末尾 · 原始 Markdown 内容保留于同一目录