10.21 水滴 AI 全栈一面:MySQL 索引连环追问与 Redis 设计
- 轮次
- 一面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 自我介绍。
- 项目的难点展开讲讲(原帖讲的是录播,以及从 XXL-JOB 换成 Spring 定时任务)。
- 看过 XXL-JOB 源码吗?
- 说说你个人的 Agent 项目,怎么做向量化的?
- 本地哈希向量化的话,如何保证准确性?有没有专门做评测?
- 项目里用过 MySQL 或 Redis 这类中间件吗?说说对 MySQL 的了解。
- 索引的设计和原理是什么?
- 索引失效的场景有哪些?(最左前缀、模糊查询、索引列使用函数、NOT IN……还有别的吗?)
- 对用户表的 userId 建了索引,用 IN 查询有时走索引有时不走,原因是什么?
- MySQL 的事务隔离级别有哪些?
- 数据库里常用的几种 log 分别用来做什么(redo log、undo log、bin log)?
- MVCC 的整体过程是怎样的?
- Redis 为什么适合做缓存?它的设计是什么样的?
- Redis 真的是单线程吗?
- Redis 的常用数据类型和适用场景?
- 这些类型的底层数据结构是什么?
- Redis 的持久化怎么做?生产上一般用哪种(RDB + AOF)?单独使用会有什么问题?
- 还用过其他什么组件?(原帖答的 Docker,被归到运维方向,没细问)
- 进程和线程的区别?
- 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 归因)。