面灵AI→

10.21 水滴 AI 全栈一面:MySQL 索引连环追问与 Redis 设计

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

《面试题目》

  1. 自我介绍。
  2. 项目的难点展开讲讲(原帖讲的是录播,以及从 XXL-JOB 换成 Spring 定时任务)。
  3. 看过 XXL-JOB 源码吗?
  4. 说说你个人的 Agent 项目,怎么做向量化的?
  5. 本地哈希向量化的话,如何保证准确性?有没有专门做评测?
  6. 项目里用过 MySQL 或 Redis 这类中间件吗?说说对 MySQL 的了解。
  7. 索引的设计和原理是什么?
  8. 索引失效的场景有哪些?(最左前缀、模糊查询、索引列使用函数、NOT IN……还有别的吗?)
  9. 对用户表的 userId 建了索引,用 IN 查询有时走索引有时不走,原因是什么?
  10. MySQL 的事务隔离级别有哪些?
  11. 数据库里常用的几种 log 分别用来做什么(redo log、undo log、bin log)?
  12. MVCC 的整体过程是怎样的?
  13. Redis 为什么适合做缓存?它的设计是什么样的?
  14. Redis 真的是单线程吗?
  15. Redis 的常用数据类型和适用场景?
  16. 这些类型的底层数据结构是什么?
  17. Redis 的持久化怎么做?生产上一般用哪种(RDB + AOF)?单独使用会有什么问题?
  18. 还用过其他什么组件?(原帖答的 Docker,被归到运维方向,没细问)
  19. 进程和线程的区别?
  20. Java 中创建线程的方式有哪些?线程池怎么用?线程池的工作流程?

《参考解析》

索引设计与原理:InnoDB 的索引是 B+ 树,非叶子节点只存键、叶子节点存数据且用双向链表串联,所以范围扫描和排序友好、层数低(千万级数据通常 3 层)。设计上按「区分度高、查询频率高、能覆盖常用列」来选:联合索引遵循最左前缀,把等值条件放前面、范围条件放后面;尽量让查询走覆盖索引避免回表;主键用自增避免页分裂。追问「为什么不用 B 树 / 哈希」时答:B 树非叶子也存数据导致扇出小、层数高;哈希只支持等值,不支持范围和排序。

索引失效:除了原帖列的最左前缀断裂、like '%x'、列上做函数或运算、NOT IN / !=,还要补:隐式类型转换(字符串列传数字,等价于在列上套了 cast)、OR 两侧有一侧没索引、联合索引中间列用了范围条件导致后面列失效、优化器估算回表成本过高主动放弃索引、ORDER BY 与索引顺序不一致产生 filesort、数据量太小或区分度太低(如性别)时全表扫描更快。

IN 查询有时走有时不走:不是 IN 本身的问题,而是优化器根据成本估算在选。常见原因:IN 列表很长时,等值匹配会退化成多次范围扫描,优化器可能判断全表扫更便宜;列表元素在索引上的分布稀疏(命中行占比高)时回表成本超过全表扫;统计信息过期导致估算失真(可用 ANALYZE TABLE 更新);IN 里的值类型与列不一致触发隐式转换;以及不同 MySQL 版本对 range 与 index 访问方式的代价模型不同。回答时给「用 EXPLAIN 看 type / rows / filtered,再对比 FORCE INDEX 的结果」这条验证路径。

三种日志:redo log 是 InnoDB 的物理逻辑日志,记录页的修改,保证崩溃恢复(WAL:先写日志再刷脏页);undo log 记录反向操作,用于回滚和 MVCC 读版本链;bin log 是 Server 层的逻辑日志,记录语句或行变更,用于主从复制与按时间点恢复。常考「两阶段提交」:redo 在 prepare、写 binlog、redo commit,保证两份日志一致。

MVCC:每行有隐藏的 DB_TRX_ID 和 DB_ROLL_PTR;更新时旧版本通过 undo log 串成版本链;ReadView 记录创建时活跃的事务 id 集合与上下界,读取时沿版本链找第一个「对当前视图可见」的版本(已提交且早于视图创建)。RC 每次查询都新建 ReadView,RR 只在事务第一次快照读时建,所以 RR 可重复读。快照读走 MVCC,当前读(SELECT ... FOR UPDATE、UPDATE)走锁。

Redis 单线程:指「命令执行」是单线程(6.0 起网络 IO 可多线程),好处是无锁、无上下文切换、天然原子;慢的原因通常是单条命令复杂度高(KEYS *、大 key 的 HGETALL)或网络往返多,所以要用 pipeline、避免大 key、把重计算放到客户端或 Lua 里控制规模。

数据类型与底层结构:String 用 SDS(记录长度、二进制安全、预分配);List 早期 ziplist + linkedlist,现在 quicklist(ziplist 分片组成的双向链表);Hash 小数据用 listpack/ziplist,大了转 dict;Set 是 intset 或 dict;ZSet 是 listpack 或 skiplist + dict(dict 存 member→score,skiplist 支持范围与排名)。答的时候顺带说「转换阈值由 *-max-listpack-* 配置控制」,比只背名字扎实。

持久化:RDB 是某一时刻的全量快照,恢复快、体积小,但会丢最后一次快照之后的数据;AOF 追加写命令,appendfsync everysec 下最多丢 1 秒,但文件大、恢复慢。生产常用「RDB 做定期全量 + AOF 做增量」,重启时优先用 AOF 恢复。单独用 AOF 的风险是文件膨胀与重写期间的内存/IO 压力;单独用 RDB 的风险是丢数据窗口大。4.0 之后还有混合持久化(AOF 重写时前半段写 RDB 格式)。

线程池流程:核心线程未满就建核心线程;满了进阻塞队列;队列满了且未到最大线程数就建临时线程;再满就执行拒绝策略(AbortPolicy 抛异常、CallerRunsPolicy 回退给提交线程、Discard、DiscardOldest)。参数要按任务类型设:CPU 密集约 核数+1,IO 密集按「核数 × (1 + 等待时间/计算时间)」估,队列要有界,否则最大线程数形同虚设。Java 创建线程的方式:继承 Thread、实现 Runnable、实现 Callable + FutureTask、线程池提交。

复盘:这场面试的节奏是「一个点顺着往下挖到答不出来为止」,MySQL 索引部分从设计问到失效再问到优化器选择,是整场的分水岭。准备时把「为什么」讲清楚比罗列结论更重要;Agent 项目的向量化被问到「本地哈希向量化怎么保证准确性」,说明面试官会怀疑非语义向量方案的召回质量,最好提前准备一份离线评测(召回率、人工标注集、badcase 归因)。