小红书产品工程师一面:2000 个账号批量拉取与 MySQL 删除空间之问
- 轮次
- 一面
- 结果
- 挂
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 自我介绍。
- 实习项目里负责的业务场景是什么?
- 手撕:2000 个账号,接口一次最多传 100 个 ID,如何快速获取全部信息且不阻塞上游服务?
- 数据库表里有 500 万条数据,用 delete 删除 200 万条,磁盘占用会变大还是变小?
- 数据库的磁盘存储结构是什么?包括哪些部分?
- 索引存储在磁盘中吗?如何存储?
- 索引在删除时会发生什么变化?
- 页合并的时机是什么?
- 索引结构变动过程中有查询会受影响吗?为什么?
- 假如数据库就是一个 Java 服务,你怎么设计页合并并保证查询不受影响?
《参考解析》
批量拉取 2000 个账号:把 2000 个 ID 切成 20 批,每批 100 个,用固定大小线程池或信号量控制并发(比如 5~10 个在飞),每批调用接口并收集结果;用 CompletableFuture.allOf 等全部完成,单批失败按批次重试,最后按原顺序拼回。关键是“不阻塞上游”:不要在请求线程里串行跑 20 次,也不要把 2000 个任务一次性丢进无界队列;要有并发上限、超时、重试与降级(失败批次返回部分结果或转异步补偿)。如果接口支持游标或分页,优先用服务端批量能力;如果下游有限流,按令牌桶控制速率,并把重试做成指数退避。还要考虑幂等和去重,避免重复请求放大下游压力。
删除 200 万行后的空间:InnoDB 里 delete 是标记删除,把记录行标记为可复用、把数据页放进空闲链表,但数据文件不会自动缩小,所以磁盘占用通常不变甚至因为 undo/redo、binlog 而变大。空间会在后续 insert 时被复用,但碎片仍然存在。要真正回收需要 OPTIMIZE TABLE(本质是重建表)或 ALTER TABLE ... ENGINE=InnoDB,线上大表要用 gh-ost/pt-online-schema-change 这类工具做在线重建,避免锁表。所以正确答案不是“变大/变小”,而是“逻辑空间释放、物理文件不一定缩小,取决于是否重建”。
磁盘结构:InnoDB 以表空间为单位,里面是段(segment)、区(extent,通常 1MB)、页(page,默认 16KB)、行。页是磁盘与内存交互的最小单位,常见类型有数据页、索引页、undo 页、系统页、流水页等。表空间文件里按页编号组织,页内用槽目录和记录链表组织行;区让页在磁盘上连续,减少随机 I/O。内存侧对应 buffer pool,读写都先经过它,脏页由后台线程刷盘。
索引存储与删除:InnoDB 的索引是 B+ 树,聚簇索引的叶子节点存整行数据,二级索引的叶子存主键值,所以二级索引查询可能要回表。索引页同样按 16KB 组织,非叶子节点只存键和子页指针,叶子节点用双向链表连接,支持范围扫描。删除索引记录时先在页内标记删除,页内记录数低于阈值(约半满)时触发页合并,把相邻页的数据合到一起并释放空页;页合并由后台或插入路径触发,不是每次 delete 立即发生。索引结构变动(页分裂、页合并)会加锁或产生 latch,查询可能短暂等待;MVCC 和 undo 保证已开始的读事务看到一致快照,但 DDL 类变更仍可能阻塞。
把数据库当 Java 服务设计页合并:核心是“后台异步整理 + 前台无锁读”。用读写分离的思路:页合并只操作空闲页或副本,先把待合并页的数据复制到新页,更新父节点的指针,再用原子引用切换;读请求走版本化快照,合并期间继续读旧页,等没有活跃读者后再回收。用引用计数或 epoch/RCU 管理页的生命周期,合并线程按低水位触发、限速,避免 I/O 抖动。对外提供可观测指标(空闲页比例、合并次数、查询 P99),并在合并失败时回滚指针。这个题考的是把存储引擎的页管理映射到并发数据结构设计,答题时先讲清楚“为什么不能原地搬”。