【面试题】1. 为什么生产环境很少用外键约束?不用外键怎么保证数据一致性?
【标准答案】外键看起来美好,生产上却有四重代价。一、性能:每次 INSERT/UPDATE 都要去主表校验,锁范围变大,高并发下吞吐直接打折。二、分库分表不兼容:数据拆到多个实例后物理上没法建外键。三、运维噩梦:删一条主表数据可能锁住整张子表,级联操作用错了就是删库级。四、数据修复困难:线上要修一条脏数据,外键拦着改不动。但我们不是"不保证一致性",而是把约束提到应用层:用代码先查询再写入、关键链路加事务包裹、定时对账脚本兜底。总结:物理外键适合内部工具和低并发场景,互联网业务一律逻辑外键 + 代码校验。
【面试题】2. 乐观锁和悲观锁分别怎么实现?什么场景用哪个?
【标准答案】悲观锁是"我先锁上,别人别动"——SELECT ... FOR UPDATE 拿行锁,事务结束才释放,适合冲突概率高的场景,比如银行转账、库存扣减的核心链路。缺点是锁时间长、并发低。乐观锁是"我先改,冲突了再说"——表里加 version 字段,UPDATE SET stock=stock-1, version=version+1 WHERE version=旧值。如果影响行数为零,说明被别人改过了,业务层重试。适合读多写少、冲突概率低的场景,比如用户信息编辑、文章点赞。我选型逻辑:涉及钱和库存用悲观锁先锁住,其他用乐观锁 + 重试。乐观锁还有一个暗坑——高并发冲突时大量重试 CPU 飙高,这时候得上悲观锁或队列串行化。
【面试题】3. EXPLAIN 里出现 Using filesort 和 Using temporary 是什么原因?怎么消除?
【标准答案】Using filesort 不是你想象的"磁盘排序",它可能只在内存里排,但本质是你的 ORDER BY 字段没走索引,MySQL 得额外做一次排序。消除办法:建联合索引让 WHERE 条件和排序字段都在索引里,让索引本身有序输出。Using temporary 是查询需要中间结果集来存 GROUP BY、DISTINCT、UNION 的结果——这就是性能杀手,因为临时表可能写磁盘。消除:让 GROUP BY 字段走索引,避免对非索引列做 DISTINCT。联合索引建对了,Extra 里干干净净只有 Using index,这就是索引覆盖,面试官最想听到这四个字。
【面试题】4. MySQL 深分页为什么越翻越慢?limit 100000,10 到底慢在哪?
【标准答案】MySQL 的 LIMIT offset,size 不是直接跳到第 offset 行开始读,而是从第一行开始逐行扫到 offset 再扔掉前 offset 行,最后返回 size 行。所以 LIMIT 100000,10 实际扫描了 100010 行,扔掉 100000 行。三个优化方案:一、游标分页(最推荐),记住上一页最后一条的 ID,下一页查 WHERE id > 上次最大id LIMIT 10,扫描行数恒定。二、延迟关联,先子查询只扫主键拿到 ID,再用 ID 回表取完整数据,减少回表开销。三、产品层面直接砍掉深分页功能,限制最多翻 100 页,99% 的用户不需要翻到第 100 页之后——这反而是最务实的解决方案。
【面试题】5. PostgreSQL 的 BRIN 索引是什么?和 B+Tree 比有什么特别优势?
【标准答案】BRIN 全称 Block Range Index,它不索引每一行,而是按连续的物理数据块做范围摘要——存每个块的最小值和最大值。查询时先排除不包含目标的块,只读可能命中那几个块。巨小:一个 BRIN 索引可能只有几百 KB,而 B+Tree 几 GB。适合什么场景?超大表、数据按插入时间自然有序的字段,比如物联网传感器日志、用户行为流水表,几 TB 数据建 BRIN 又省空间又快。不适合什么?数据乱序存储的字段,BRIN 没命中要扫很多块反而慢。一句话:BRIN 是"瘦索引",牺牲精度换空间,在日志和时序场景下性价比碾压 B+Tree,这是 PG 的独门绝活。
【面试题】6. Redlock 分布式锁到底怎么回事?为什么有人说它不安全?
【标准答案】Redlock 是 Redis 作者提出的多节点分布式锁方案:客户端同时向 N 个独立 Redis 节点请求加锁(不经过主从复制),过半成功、总耗时小于锁有效期才算获取成功。争议来自 Martin Kleppmann 的论文:Redlock 依赖时间来判断锁有效期,但系统时钟可能跳跃、GC 停顿让锁超时判断失效,可能导致两个客户端同时认为自己持有锁。Redis 作者的反驳焦点是:fencing token 可以补救——获取锁时带上单调递增 token,下游资源检查 token 拒绝旧锁。生产实践:非关键场景直接用单节点 Redis + Redisson 的看门狗续期锁,够用;关键业务(资金类)别纠结 Redis,直接用 ZooKeeper 或 etcd 的 CP 强一致锁。
【面试题】7. 前缀索引和覆盖索引有什么区别?分别在什么场景下用?
【标准答案】前缀索引是对长字符串字段只索引前 N 个字符——省空间、高扇出,但代价是 ORDER BY 和 GROUP BY 用不了这个索引,因为索引不完整没法排序。适合 email、URL 这类长字段的搜索,前缀区分度够高就行。覆盖索引是查询的 SELECT、WHERE、ORDER BY、GROUP BY 所有列都在一个联合索引里,Extra 显示 Using index,读完索引直接返回不用回表——这是索引查询的最高境界。实战:用户表经常查 name+email,建 (name, email) 联合索引,SELECT name, email FROM user WHERE name='张三' 就是完美覆盖。原则:长字段先考虑前缀索引节省空间,高频查询构建覆盖索引消除回表。
【面试题】8. MongoDB 分片集群的 Chunk 是怎么分裂和迁移的?分片键选错了会怎样?
【标准答案】MongoDB 分片集群有三层:mongos 路由、Config Server 存元数据、Shard 存数据。数据按分片键范围切分为 Chunk(默认 64MB),一个 Chunk 超大小自动分裂为两个。Balancer 后台进程检测各 Shard 的 Chunk 数,不均匀就把 Chunk 从多的 Shard 迁到少的 Shard——迁移期间该 Chunk 还是可读写的,对业务基本透明。但如果分片键选错了,一两个 Chunk 巨热(热点),其他 Shard 闲死,加再多的 Shard 也白搭。分片键挑选三大原则:高基数(别像性别只有男/女)、均匀分布、查询能带上。实战最推荐的是组合字段,比如 user_id+timestamp,读写都能精准落分片。
【面试题】9. MapReduce 的 Shuffle 阶段到底做了什么?为什么说是性能瓶颈?
【标准答案】Shuffle 是 Map 到 Reduce 之间的数据重分布过程,分 Map 端和 Reduce 端。Map 端:每个 Map 任务把输出按 Reduce 数量分区,先写内存缓冲区,满了溢写到磁盘——溢写前会在内存里先排序,同一个分区的数据写成一个溢写文件,所有溢写文件最后归并成一个分区文件。Reduce 端:从各个 Map 任务的机器上通过 HTTP 拉取自己分区的数据,拉过来后先归并排序再分组,最后喂给 reduce 函数。瓶颈原因很明显:Map 端多次溢写有磁盘 IO、Reduce 端跨网络拉数据、全量排序很吃 CPU。优化三板斧:加 Combiner 做 Map 端预聚合减少传输量、调大缓冲区减少溢写次数、调大并行度让 Shuffle 更分散。面试官想听你点到 Shuffle 是 MR 最重的环节。
【面试题】10. Spark 的 Catalyst 优化器是怎么把一个 SQL 变成高效执行计划的?
【标准答案】Catalyst 四步流水线。第一步解析:把 SQL 字符串解析成 Unresolved Logical Plan,只是 AST 语法树,表和列还没绑定。第二步分析:查 Catalog 绑定表名、列名、类型,变成 Resolved Logical Plan。第三步逻辑优化——这是核心:应用大量规则,谓词下推把 WHERE 条件推到最靠近数据源的位置、列裁剪只读需要的列、常量折叠把 1+2 直接算成 3、投影合并消除多余的投影操作。第四步物理优化:根据数据量统计信息选择策略——小表 join 用 BroadcastHashJoin 广播到所有节点、大表 join 用 SortMergeJoin。最后生成 RDD 代码或 Tungsten 字节码执行。一句话:Catalyst 让 Spark 能"理解"你的 SQL,帮你自动优化,所以写 Spark SQL 往往比手写 RDD 更快。