场景设计面试
场景设计面试
【中等】如何在 10 亿个数据中找到最大的 1 万个?⭐⭐⭐
构建容量大小为 1 万的堆,每次从 10 亿数据中读 1 万条数据,写入最小堆,循环直至读完所有数据。最终,还留存在最小堆中的数据就是 TOP 10000
【中等】有几台机器存储着几亿的淘宝搜索日志,假设你只有一台 2g 的电脑,如何选出搜索热度最高的十个关键词?⭐⭐
核心思想:分而治之 + 哈希分桶 + 堆排序
第一步:哈希分桶(分散数据)
- 操作:逐行读取几亿条日志,对每个搜索词计算哈希值,然后取模 N(例如 N=200),将记录追加写入对应的临时文件(
part_0.txt到part_199.txt)。 - 目的:相同的关键词一定会进入同一个临时文件,且每个文件大小可控(如几十到几百 MB)。
- 记忆点:“哈希取模分文件,同词同桶不乱窜。”
第二步:内存统计(桶内计数)
- 操作:依次读取每个临时文件,将其全部加载到内存,用
HashMap统计每个词的出现次数(词频)。 - 结果:每个临时文件生成一个结果文件(
result_0.txt),每行格式关键词 次数。 - 注意:因每个文件较小(≤2G 内存),HashMap 可完全容纳。
- 记忆点:“小文件全读入,哈希表里计次数。”
第三步:堆排取 Top(全局筛选)
- 操作:维护一个大小为 10 的最小堆(堆顶是当前第 10 大的次数)。
- 遍历所有结果文件的每一行,取出
次数与堆顶比较:- 若
次数 > 堆顶,则弹出堆顶,插入该词; - 否则跳过。
- 若
- 遍历所有结果文件的每一行,取出
- 目的:只需遍历一次所有中间结果,内存只存堆(10 个元素)。
- 记忆点:“最小堆里比大小,遍历一遍取前十。”
第四步:输出结果
- 堆中剩下的 10 个词即为搜索热度最高的关键词。
【中等】一张表里有三个字段(id,开始时间,结束时间),表中数据量为 5000W,如何统计流量最大的时候有多少条数据?⭐⭐
可以采用差分数组来实现。
可以通过将每个事件分开的开始时间和结束时间记录为增量(开始时流量 +1,结束时流量 -1),并通过扫描线的方式对每一秒进行累加,最终得到每秒的并发流量。
【中等】有 40 亿个 ID,在 1G 内存中进行去重,如何实现?⭐⭐⭐
核心思想
用位图(Bitmap)标记每个 ID 是否出现过。
一个 bit 代表一个 ID,内存足够,速度极快。
条件
- ID 是 32 位无符号整数,范围
0 ~ 2^32-1(约 42 亿)。 - 因为最大 ID 约 42 亿,所以需要 42 亿个 bit 来标记每个 ID 是否出现。
内存计算
2^32 bits = 512 MB(2^32 / 8 / 1024 / 1024 = 512)。- 1G 内存 > 512MB,完全可行。
步骤
- 初始化:申请一个 512MB 的
byte数组(或bit数组),所有位初始为 0。 - 遍历:依次读取每个 ID,计算其在位图中的位置:
byteIndex = id / 8bitIndex = id % 8- 将对应位设为 1(
byte[byteIndex] |= (1 << bitIndex))。
- 结果:所有被标记为 1 的位对应的 ID 即为去重后的 ID。
优缺点
- 优点:速度快(O(n)),精确,内存可控。
- 缺点:只适用于整数且范围已知,若 ID 范围超过 42 亿(如 64 位),则内存爆炸(需要 2^64 bits,不可能)。
【中等】如果有 500G 数据需要排序,但是只有 4G 内存,如何实现?⭐⭐⭐
核心思想:分块排序 + 多路归并(分而治之)
第一步:分块排序
- 将 500G 数据分成若干个小块,每块大小小于 4G(如 3.5G),保证能加载到内存。
- 对每块数据在内存中进行内部排序(如快速排序)。
- 将排序后的块写回磁盘,成为有序的临时文件。
第二步:多路归并
- 同时打开所有有序临时文件,每个文件分配一个输入缓冲区(如几十 MB)。
- 使用最小堆从所有缓冲区的当前元素中选出最小值,输出到输出缓冲区。
- 当某个输入缓冲区读完时,从对应文件继续读取下一批数据;当输出缓冲区满时,写入最终结果文件。
第三步:清理
- 所有数据归并完成后,删除临时文件。
关键优化点
- 缓冲区大小:合理分配内存,既要保证输入缓冲够用(减少磁盘 I/O),又要留足够空间给堆。
- 堆优化:使用最小堆减少比较次数,提升归并效率。
- 并行 I/O:读写磁盘可异步进行,或使用多线程重叠计算与 I/O。
- 压缩:临时文件可压缩存储,减少磁盘占用和 I/O 量。
【中等】如何导入百万数据量级的 Excel 到数据库?⭐⭐⭐
流式读取 + 分批处理 + 批量插入 + 异步化
第一步、流式读取 Excel
- 问题:一次性读取百万行到内存会 OOM。
- 方案:使用支持流式读取的库(如 EasyExcel、POI 的 SXSSFWorkbook 读模式),逐行读取,逐行处理。
- 记忆点:EasyExcel 流式读,一行一行慢慢来。
第二步、数据校验与转换
- 逐行校验:对每行数据进行格式、合法性校验(如必填、类型、长度)。
- 转换:将 Excel 的行数据转换为数据库实体对象。
- 记忆点:一行一校验,转换不拖延。
第三步、分批缓存与批量插入
- 设定批大小:如每 1000 条记录攒一批,达到批大小后执行一次批量插入。
- 批量插入:使用 JDBC 的 addBatch() 或 MyBatis 的 batch 模式,减少网络和事务开销。
- 事务控制:一批一个事务,避免大事务导致锁竞争。
- 记忆点:千条一批批量插,事务分开保速度。
第四步、异步化与进度反馈
- 异步导入:前端提交导入任务后立即返回任务 ID,后端异步处理。
- 进度反馈:后端定期更新任务进度(如已处理行数),前端轮询展示。
- 记忆点:异步任务不阻塞,进度反馈看得见。
第五步、错误处理与补偿
- 错误记录:将校验失败或插入失败的行记录到错误表或日志文件,并提供下载。
- 断点续传:如果导入过程中断,可记录已成功行数,下次从中断处继续。
- 记忆点:错误行记下来,断点续传更可靠。
性能优化要点
- 数据库连接池:配置合适的连接数。
- 索引策略:导入前可暂时禁用非必要索引,导入后重建。
- 写入方式:使用 JDBC 的 rewriteBatchedStatements=true(MySQL)。
- 内存控制:流式读取 + 批处理,确保内存稳定。
【中等】Excel 导出场景很慢,如何优化?⭐⭐⭐
Excel 导出慢一般有三种情况:
- 数据库查询慢
- 业务逻辑处理慢
- Excel 文件写入慢
数据库查询优化
- 痛点:
LIMIT offset, size分页在 offset 很大时性能极差,需扫描大量无效行。 - 方案:游标分页(基于主键 ID)
- 记录上一批最后一条的 ID,下次查询从该 ID 之后开始。
- SQL 示例:
SELECT * FROM orders WHERE status = 1 AND id > ? ORDER BY id ASC LIMIT 1000
- 前提:ID 字段有索引,且查询必须带
ORDER BY id ASC,让 MySQL 走索引顺序扫描。 - 优势:每次只扫描固定行数,性能稳定,速度提升数百倍。
业务逻辑优化
- 痛点:循环中多次调用 RPC / 缓存,导致网络 IO 次数爆炸,耗时剧增。
- 方案:批量查询
- 先从列表中提取所有查询条件(如用户 ID),一次性批量调用
batchGetUserByIds。 - 再用结果 Map 回填数据,将 N 次 RPC 降为 1 次。
- 先从列表中提取所有查询条件(如用户 ID),一次性批量调用
- 优势:大幅减少网络交互,显著提升接口响应速度。
Excel 生成优化
- 痛点:POI 等传统框架将全量数据加载到内存,大数据量导出易导致 OOM。
- 方案:EasyExcel 流式写入 + 分批查询
- 流式写入:边查边写,每批数据刷入磁盘,内存占用稳定在 100-200 MB。
- 分批查询:结合游标分页,每次查询 1000 行,避免全量加载。
- 进阶:超大数据量可采用多线程分片导出,按地区 / 时间分片,并发生成多个 Sheet 或文件,最后打包 ZIP。
- 优势:避免 OOM,支持超大数据量导出,内存占用可控。
【中等】假设生产者-消费者模型中有 200 万个生产者,只有 1 个消费者,如何实现?⭐⭐
挑战和目标
核心挑战
- 200 万并发写:直接竞争同一个队列会导致严重锁冲突、CPU 飙升。
- 单消费者:消费速度可能成为瓶颈,需最大化消费者效率。
- 内存压力:海量数据堆积可能撑爆内存。
设计目标:高吞吐、低延迟、背压保护、数据不丢失(按需)
生产者优化
问题:200 万个生产者如果直接写入同一队列,竞争激烈。
方案:采用无锁环形队列 + 多生产者序号分配,典型实现是 Disruptor。
- Disruptor 原理:
- 预先分配固定大小的环形缓冲区(RingBuffer)。
- 每个生产者通过 CAS 竞争获取下一个可写入的序号,然后写入数据。
- 消费者通过序号屏障读取。
- 优点:无锁、预分配内存、缓存行填充避免伪共享,性能极高。
- 记忆点:Disruptor 环形缓冲区,CAS 分配序号,多生产者无锁写入。
消费者优化
问题:单消费者处理速度必须跟上 200 万生产者的写入速度。
方案:消费者内部批量处理 + 异步化。
- 批量拉取:消费者每次从环形缓冲区拉取一批数据(如 1000 条),减少调用次数。
- 多线程处理业务:消费者收到一批数据后,提交给线程池并行处理,但需保证消费顺序(如需)。
背压机制
问题:生产者写入速度超过消费者处理速度,数据堆积。
方案:缓冲区满则阻塞,监控阈值防溢出。
- Disruptor 等待策略:当 RingBuffer 满时,生产者可配置阻塞、自旋或抛出异常。
- 拒绝策略:若堆积超过阈值,可降级处理(如丢弃非关键数据、记录日志)。
- 监控告警:实时监控 RingBuffer 使用率,触发扩容或限流。
生产者线程管理
问题:200 万个生产者如果是独立线程,系统无法支撑。
方案:生产者非线程池,IO 模型限并发,事件驱动写队列。
- 使用 NIO 或 Reactor 模型(如 Netty)处理海量连接,每个连接的事件由少量 IO 线程处理,这些线程作为生产者写入队列。
- 若生产者是业务线程,则使用线程池限制并发数,由有限线程代表 200 万任务写入。
【中等】如何对 500 万会员提前 7 天进行过期提醒?⭐⭐⭐
核心思路是:定时任务 + 索引优化 + 异步通知
- 在会员表的
expire_date字段上建立索引。 - 设置一个定时任务,定期滚动翻页扫描表记录,筛选 7 天后过期的数据。
- 对过期会员发送提醒,并在记录表中标记状态为已提醒。
【困难】如何设计一个消息推送系统(千万级用户)?⭐⭐⭐
需求拆解:千万级用户在指定时间收到 Push/短信/站内信,分钟级触达,不重不漏。
整体架构:任务调度 → 人群圈选 → 消息生产 → MQ 削峰 → 发送集群(分通道)→ 状态回传与统计。
核心设计
- 人群圈选:定时任务扫描(索引 expire_date)或用圈人服务(标签/位图)产出目标用户列表,分片存储(如按 userId hash 切 1000 片)。
- 削峰与并发:目标用户 ID 分片写入 MQ,发送集群多实例并行消费;按通道(iOS/Android/短信)分队列,限速保护第三方通道配额。
- 防重与幂等:任务 ID + 用户 ID 作为幂等键(Redis SETNX 或 DB 唯一索引),任务重跑不重复推送。
- 失败重试:发送失败进重试队列,指数退避重试 3 次,仍失败进死信队列人工介入;短信通道失败可自动切换备用通道。
- 防骚扰与合规:全局频控(单用户单日推送上限)、免打扰时段过滤、退订名单过滤。
- 状态闭环:记录送达/点击回执,实时统计触达率,异常骤降触发告警熔断。
量化:千万用户 / 5 分钟送达 ≈ 3.3 万条/秒,需多通道并行 + 批量接口(如每批 100 条);发送集群按通道配额反推实例数。