面灵AI→

拼多多服务端开发一面:SQL 优化、MySQL、Redis、MQ 与手撕算法

轮次
一面
时间
2026-10
来源
牛客网

《面试题目》

一、实习项目与 SQL 优化

  1. 请简单做一下自我介绍。
  2. 看到你简历中有一段 MySQL 查询优化的经历,具体是怎么优化的?
  3. 你提到将多条 SQL 聚合成一条,具体是统计什么业务指标?
  4. 这些指标是每天都需要计算,还是只需要计算一次?
  5. SQL 聚合查询有没有使用窗口函数?
  6. 除了项目中提到的优化方案,还有没有做过其他 SQL 优化,例如慢查询优化、索引优化等?

二、MySQL

  1. 如果线上出现一条慢 SQL,你会怎么排查和优化?
  2. 使用 EXPLAIN 分析 SQL 时,重点关注哪些字段?
  3. 假设现在有一条 SQL,包含 user_id、state 两个等值查询条件,以及 create_time 范围查询和排序,你会怎么设计索引?为什么?
  4. 哪些情况下会导致 MySQL 索引失效?
  5. 如果接触一个全新的业务,需要从零开始设计数据库表,你会怎么设计?
  6. 数据库设计时,为什么有时候需要增加冗余字段?
  7. 假设用户表和地址表存在关联关系,是否可以直接在用户表中冗余存储地址信息?
  8. 增加冗余字段是否会破坏数据库范式?
  9. 数据库范式和实际业务需求发生冲突时,你会怎么权衡?

三、Redis

  1. Redis 常见的数据结构有哪些?String、Hash、List、Set、ZSet 分别适合什么使用场景?
  2. 如果使用 Redis 作为缓存,如何解决缓存和数据库的数据一致性问题?
  3. 先更新数据库再删除缓存,是否还会出现数据不一致?有哪些场景?
  4. 除了延迟双删,还有没有其他缓存一致性解决方案?
  5. 如果通过监听 MySQL Binlog,再经过消息队列处理缓存更新,这种方案有什么好处?

四、消息队列

  1. 了解消息队列的设计原理吗?
  2. 使用消息队列时,如何保证消息不会丢失?
  3. 如果消费者处理一条消息到一半时突然宕机了,应该怎么处理?
  4. 如何保证消息不会漏消费,同时避免重复消费造成业务问题?
  5. 消息消费的幂等性通常怎么实现?

五、Spring

  1. Spring 中的 @Transactional 在哪些情况下会失效?

六、手撕算法

  1. 二分插入排序。
  2. 任务调度 / 最少线程数(力扣 253)。

《参考解析》

慢 SQL 的排查链路

不要一上来就加索引,先确认「慢」的定义和范围:是偶发还是持续、是某条 SQL 还是整个接口、QPS 多少、数据量多大、慢在数据库还是慢在应用侧连接池或网络。数据来源分两类——事后看慢查询日志(long_query_time 阈值调低一点,按 Query_time 和扫描行数排序,找出 Top SQL),事前用 EXPLAIN 或 EXPLAIN ANALYZE 看执行计划。

看执行计划要重点盯:type(ALL 全表扫、index 全索引扫、range、ref、const,最好到 ref 以上)、key 与实际用的索引、rows 预估扫描行数、filtered 过滤比例、Extra(Using filesort、Using temporary、Using index condition 分别说明排序没走索引、用了临时表、以及索引下推)。如果预估行数和实际差很多,说明统计信息过期,ANALYZE TABLE 一下。

定位到之后按性价比排序处理:建合适的联合索引或覆盖索引(让查询只走索引不用回表)、改写 SQL(避免函数作用在索引列上、避免 %like 前置、把大 in 拆小、分页用游标而不是大 offset)、减少返回列与数据量、冷热分离与归档。如果再不行才考虑物化中间结果、读写分离、或者上缓存。改完必须回看监控指标——包括那条 SQL 的耗时和整个库的 CPU、IOPS,避免把压力转移而不是消除。

等值 + 范围混合的联合索引怎么设计

题目给的是 user_id、state 两个等值条件 + create_time 范围 + 按 create_time 排序。联合索引的设计依据是两条经典规则:等值条件在前、范围条件在后,以及索引列顺序要能同时服务过滤与排序。

所以优先考虑 (user_id, state, create_time):user_id 和 state 两个等值列先定位到一个很小的范围,再在剩下的连续区间里按 create_time 做范围扫描——因为在这个索引里,同一组 (user_id, state) 内部 create_time 是有序的,范围过滤和 ORDER BY create_time 可以同时走索引,省掉 filesort。

如果 state 的基数很低(比如只有几个状态),把它放在等值列第二位收益有限、还会让索引变宽,也可以只建 (user_id, create_time),让 state 回到回表后过滤,具体要看选择性数据。判断标准永远是选择性(区分度)× 使用频率,不是列的顺序好看。另外要问清查询是否固定带 user_id:如果存在「只按 state + 时间」的查询,就得再补一条索引,或者用覆盖索引把常用字段带出来。

先更新数据库再删除缓存,为什么还会不一致

这个顺序比「先删缓存再更新库」好,但仍有几个典型漏洞:

  1. 并发读回填:请求 A 发现缓存未命中,从库里读到旧值;此时请求 B 更新库并删除缓存;A 才把旧值写回缓存——缓存从此长期是旧值,且不会有任何人再来修正它。这类不一致最难发现,因为它是「静默长期错」。
  2. 删除缓存失败:更新库成功、删缓存这一步超时或异常,如果业务没有重试机制,缓存就一直是旧值。
  3. 主从延迟:写走主库、读走从库,删缓存后立刻有请求打到从库,读到尚未同步的旧数据又回填。
  4. 事务未提交就删缓存:删缓存在事务提交前执行,提交前的读请求把旧值写回。

缓解手段按强度递增:给缓存设过期时间(兜底,接受最终一致)、延迟双删(更新前后各删一次,中间 sleep 一段时间覆盖读回填窗口,但 sleep 时长的经验值难定、且不可靠)、读写都加分布式锁/单飞(防止回填旧值)、用 Binlog 订阅驱动缓存失效(应用只负责改库,缓存删除由消费 Binlog 的下游执行,天然在事务提交后触发、失败可重试、还能跨服务统一处理,缺点是多了一条链路和延迟)。

最后要给结论:在并发读写场景下,缓存与数据库不存在既高性能又强一致的免费方案,工程上的目标是把不一致窗口压到可接受范围,并让业务对不一致有容忍度——回答里能主动点出这一点,比背方案更得分。

消息队列怎么保证不丢、不重

「不丢」要分三段分别看:生产者丢失——同步发送并处理返回、或用带回调的异步发送 + 本地消息表/事务消息保证「业务提交」和「消息发出」二选一必成;Broker 丢失——刷盘策略(同步刷盘 vs 异步)、多副本与 acks=all 之类的确认级别、以及节点故障时的副本选举;消费者丢失——关闭自动提交位点,改成业务处理成功后再手动 ack,否则「先 ack 后处理、处理到一半宕机」这条消息就永久丢了。

消费者处理到一半宕机,本质上就是位点没提交、消息会被重新投递,所以要求业务侧可重放:处理逻辑要幂等,或者把「业务写入」和「位点提交」放进同一个本地事务/同一张表。

「不重」在分布式投递语义里只能做到至少一次 + 消费幂等。幂等的常用实现:用业务唯一键(订单号、消息 ID)建唯一索引,重复插入直接冲突忽略;用 INSERT ... ON DUPLICATE KEY UPDATE 或状态机判断(只在特定前置状态下才允许流转);把消息 ID 存 Redis 做去重窗口;或者用版本号/乐观锁防止旧消息覆盖新数据。注意去重窗口是有代价的(存储 + 内存),要跟消息量和重试窗口匹配。

@Transactional 失效的常见场景

按「代理没生效」和「事务没按预期回滚」两类记:

代理没生效——同类内部自调用(this.method() 不经过代理,事务注解形同虚设,解决办法是注入自身代理、拆到另一个 Bean、或用 AopContext.currentProxy());方法不是 public(Spring AOP 默认只代理 public 方法);类没有被 Spring 管理(手动 new 出来的对象);切面顺序问题(比如被自定义 AOP 包裹,异常没传到事务拦截器)。

事务没按预期工作——异常类型不对:默认只对 RuntimeException 和 Error 回滚,受检异常需要显式写 rollbackFor = Exception.class;异常被自己 catch 掉没有重新抛出;传播行为选错(REQUIRES_NEW、NESTED 与预期的边界不同,NOT_SUPPORTED 会让当前方法根本没事务);多数据源/多事务管理器没指定 transactionManager;只读事务里执行了写操作;以及事务里做远程调用或耗时操作导致的连接占用与超时。还有一个容易被忽略的:事务方法里把异常包装成了新的异常但丢失了原始类型,回滚规则匹配不上。

手撕:二分插入排序与最少会议室

二分插入排序的要点是在已排序区间里用二分找插入位置,再整体后移。写的时候注意三点:二分边界统一用「左闭右开」或「左闭右闭」其中一种,不要混;插入位置取的是第一个大于目标值的位置(用 lowerBound 语义,保证稳定性);后移要用 System.arraycopy 或从后往前倒着搬,别写成从前往后的循环把数据覆盖了。复杂度是比较次数 O(n log n)、移动次数仍是 O(n²),所以整体 O(n²),适合数据量小或近乎有序的场景。

力扣 253(最少会议室 / 最少线程数)的经典解法是排序 + 小根堆:把所有区间按开始时间排序,用一个堆维护「当前正在占用的会议的结束时间」;遍历每个区间,如果堆顶的结束时间小于等于当前区间的开始时间,说明那个会议室已经空了,弹出复用;把当前区间的结束时间压入堆;遍历结束后堆的大小就是所需的最少会议室数。复杂度 O(n log n)。

另一个等价解法是差分/扫描线:把所有开始时间记为 +1、结束时间记为 -1,排序后从左到右累加,过程中出现的最大值就是答案(注意「结束时间 == 下一个开始时间」时是否算重叠,要按题目口径决定是先处理 −1 还是 +1)。面试里可以两个都说:堆的做法直观、扫描线的做法代码更短,但扫描线对「同一时刻先加还是先减」的边界更敏感。