后端进阶-性能优化
相关文档:
一、性能优化
1.MySQL 中如何进行 SQL 调优?
核心思路:先找慢查询→看 EXPLAIN→补/调整索引→重写 SQL → 如果仍不行考虑分区/缓存/物化视图。
背景:SQL 慢通常来自错误的索引、低效的访问路径、数据量激增或不合理的 SQL 写法。
排查思路(步骤)
找到慢 SQL(慢查询日志 / APM / pt-query-digest / performance_schema)。
用 EXPLAIN [FORMAT=JSON] <sql> 看执行计划(访问顺序、使用索引、type、rows、extra)。
用 SHOW CREATE TABLE t / SHOW INDEX FROM t 看表结构与索引。
用 ANALYZE TABLE t / OPTIMIZE TABLE / ANALYZE TABLE 确保统计信息是最新的。
如果复杂,EXPLAIN ANALYZE <sql>(MySQL 8 有)可以看到真实行数与时间。
若涉及锁/等待,查看SHOW PROCESSLIST,INFORMATION_SCHEMA.INNODB_TRX/LOCKS。
-- 找慢查询(示例)
SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';-- 执行计划
EXPLAIN FORMAT=JSON SELECT ...;
EXPLAIN SELECT ...;-- 查看索引
SHOW INDEX FROM db.table;-- 分析表统计
ANALYZE TABLE db.table;
常见调优手段(具体)
加/调整索引:把 where/order by/join 用到的列建立合适复合索引(注意列顺序)。
覆盖索引(Covering Index):SELECT 的列都能从索引中直接返回,避免回表。
避免 SELECT *:只选必要列,减少 IO。
拆大查询:把复杂聚合拆成临时表分步计算,或使用物化视图。
重写 SQL:把 IN/子查询改成 JOIN(或反之,视情况),避免函数对列的使用(索引失效)。
分区/分表:数据量巨大的表考虑按时间或范围分区,减少单表扫描。
缓存层:热点查询结果放 Redis 缓存(注意一致性)。
2.MySQL 中使用索引一定有效吗?如何排查索引效果?
核心思路:索引是否生效要看 EXPLAIN 的 key
字段和预计扫描行数,若未用索引应检查查询写法、函数使用、列顺序与统计信息。
索引可能不生效的常见原因
WHERE 中对列使用函数/表达式(WHERE DATE(col)=...),会使索引失效。
列顺序不对:复合索引需要按最左前缀原则使用。
数据分布或统计信息过时 → 优化器选择全表扫描。
ORDER BY / GROUP BY 用到非索引列导致 filesort。
排查方法 / 命令
EXPLAIN FORMAT=JSON SELECT ...; 看 key、possible_keys、rows、filtered。
ANALYZE TABLE t; 更新统计信息后再 EXPLAIN。
SHOW INDEX FROM t; 查看索引列、cardinality(基数)。
EXPLAIN ANALYZE(MySQL8)观察真实行数与时间。
用 pt-index-usage / pt-query-digest 分析索引未命中情况(工具)。
优化建议
为 WHERE/JOIN/ORDER BY 的关键列建立复合索引,遵循最左前缀原则。
避免在索引列上做计算/函数,改写查询或增加冗余列(预计算列)并建立索引。
若选择性低,考虑不建索引或使用联合索引把低选择性列放在后面。
3.深度分页问题如何解决?
深度分页是指在分页查询中,当
offset
(偏移量)非常大时(比如LIMIT 10 OFFSET 1000000
),数据库需要先扫描并跳过大量数据,再返回少量结果,导致的性能下降问题。
核心思路:深度分页避免 OFFSET,使用 keyset 或预计算/缓存来保证性能。
背景:OFFSET + LIMIT 在大页数时性能灾难(需要跳过大量行,IO/CPU 成本高)。
常用解决方案
1.Keyset Pagination(基于游标/seek) — 推荐
用WHERE (id < last_id) ORDER BY id DESC LIMIT N。不使用 OFFSET,查询走索引,性能稳定。
2.基于索引的分页(利用复合索引)
当 ORDER BY 是复合索引的前缀,可使用范围查询替代 OFFSET。
3.物化分页 / 预计算页
定期把常见页缓存到 Redis / precomputed_pages 表。适合只读或变化不频繁的场景。
4.延迟加载 / 搜索引擎
用 Elasticsearch 处理分页查询(尤其带过滤和排序)。
5.分区 + 子查询
分区表根据某条件先缩小范围,再分页。
6.倒序 + LIMIT 替换深度分页
如果用户只往后翻,用seek方法更好。
示例(Keyset)
-- 第一次请求
SELECT * FROM orders WHERE user_id=1 ORDER BY created_at DESC LIMIT 20;
-- 返回最后一条 created_at = '2025-09-01 12:00:00'
-- 下一页
SELECT * FROM orders WHERE user_id=1 AND created_at < '2025-09-01 12:00:00' ORDER BY created_at DESC LIMIT 20;
4.如果现在有 500G 数据需要排序,但是你只有 4G 内存,如何实现?(外排)
思路:内存不足就做外部归并排序(External Merge Sort),分为两个阶段:分割(生成有序子文件)和归并(合并子文件)。
步骤
分割:分块读入(每次读不超过 4G 的可用内存,比如 100MB 块),在内存里排序(快速排序/堆排序),输出到磁盘生成若干有序小文件(runs)。
归并:用 k 路归并(k 根据可打开文件数与内存缓冲决定),把若干 runs 逐步合并为更大的有序文件直到最终只有一个有序文件。归并时使用最小堆维护 k 路当前最小元素。
优化:使用多级归并(多轮),尽量让 IO 顺序化(顺序读写),压缩中间文件以减少 IO。
工具与实现
在 Linux 用 sort 能自动外排:sort -T /tmp -S 2G input > output(-S 指定内存)
自实现:分块排序写文件 → 使用优先队列进行 k-way merge → 输出。
对大数据量,考虑 MapReduce / Spark 的 sort 或使用外部数据库系统。
面试要点:描述 external merge sort 的两阶段(分块排序 + k-way 归并),并指出 IO 优化与内存/文件句柄限制。
5.假设 Redis 内存溢出,如何排查与解决?
核心思路:先定位大 key / 热点 / 策略,再做短期救火(扩容/删除/调整策略),中长期做分片、结构优化与冷热分离。
排查步骤
1.查看 Redis 状态与内存指标
redis-cli INFO memory
redis-cli INFO stats
redis-cli CONFIG GET maxmemory
redis-cli --bigkeys # 查大 key
redis-cli --scan | xargs -n1 redis-cli TYPE # 慎用
2.定位占用大的 key
--bigkeys 找到大 key(hash、list、zset、string)。
MEMORY USAGE key 查看单 key 内存占用。
MEMORY STATS/MEMORY DOCTOR 获得内存诊断。
3.检查 eviction 策略
CONFIG GET maxmemory-policy(如 allkeys-lru, volatile-lru 等)
若设置为 noeviction,当超出会报错。
4.检查瞬时峰值
比对指标历史(监控)看是长期超载还是瞬时爆发(热点/thundering herd)。
5.查看键过期与 TTL
检查是否大量 key 无 TTL 导致累积。
解决方案(短中长期)
短期(快速缓解)
增加实例内存或临时扩容 Redis 节点(vertical/horizontal)。
手动删除非必要大 key(慎重),或者使用 FLUSHDB(极端)。
调整 maxmemory-policy 到合适 LRU 策略。
中期
热点 Key 控制:采用本地缓存(Caffeine)、限流或预聚合减少单 key 压力。
数据结构优化:把 string 大值转为 hash(hash 小对象内存更省),使用压缩(ziplist/quicklist tunings)。
设置合理 TTL:长久不使用的数据加 TTL。
分片/Cluster:拆成 Redis Cluster 或 Sharding,减小单节点负担。
长期
冷热分离:热数据放 Redis,冷数据放后端 DB 或对象存储。
监控/告警:内存占用、key 增长率及 top keys 告警。
避免大对象规范:对大列表、ZSet 做分页或按时间窗口切割。
6.接口变慢了应该如何排查?导致接口变慢的常见原因有哪些?
核心思路:找到慢点(trace)→ 是 DB / 依赖 / JVM / 资源瓶颈?→ 针对性优化(索引/缓存/限流/异步/GC 调优/扩容)。
排查步骤(从宏到微)
重现并量化:确认是否普遍变慢或仅个别请求,查看 APM(分布式追踪)/Grafana 指标(p50/p95/p99/QPS)。
看链路追踪(Trace):找到耗时最大的 span(是 DB、RPC、序列化、GC 还是网络)。
查看资源使用:CPU、内存、磁盘 IO、网络带宽、线程池、连接池使用情况。top / iostat / vmstat / netstat / ss / jcmd jstack / jmap/jstat。
查看数据库:慢查询日志、当前锁、连接池是否满、事务阻塞(SHOW PROCESSLIST、INNODB_TRX)。
检查外部依赖:第三方服务/下游服务是否降级或超时(查看超时/重试链路)。
查看日志 & 错误率:是否有异常堆栈、大量重试/超时导致放大。
检查部署/配置变更:最近有没有发布、依赖库升级、配置改动、证书/网络变更。
线程/GC 分析(Java):jstack 看死锁/阻塞,GC 日志查看 Full GC 导致停顿。
常见原因(按出现频率)
数据库慢(慢 SQL、索引失效、锁等待、连接池耗尽)。
热点 key / 缓存穿透(导致后端压力瞬增)。
外部依赖变慢(RPC/第三方支付/存储)。
线程池/队列阻塞(线程用尽导致请求排队)。
GC 或内存泄漏 导致 Stop-the-world 或频繁 Full GC。
网络/IO 瓶颈(磁盘、网卡、负载均衡器故障)。
部署回滚/配置错误(比如把日志级别设 DEBUG 导致 IO 变慢)。
事故级别流量突增(促销/爬虫/攻击)。
解决思路(常见策略)
若是 DB:优化 SQL / 加索引 / 分库分表 / 加缓存 / 提高连接池。
若是缓存穿透/爆发:加本地 cache、熔断、限流、后压(drop/queue)。
若是外部依赖:设置合理超时 + 重试 + 熔断(circuit breaker),并做降级策略。
若是线程池耗尽:检查任务执行时间、拆小任务、扩容线程池或引入异步处理。
若是 GC:分析堆栈、调整堆大小与 GC 策略,修复内存泄漏。
若是网络:检查链路、LoadBalancer 后端健康、扩容或路由优化。
工具
APM(SkyWalking / Zipkin / Jaeger / NewRelic)
系统工具:top/iostat/vmstat/ss
Java 工具:jstack/jmap/jstat/jcmd
DB 工具:慢查询日志、pt-query-digest、EXPLAIN/ANALYZE
二、补强题目
1.MySQL 数据库一亿条数据,怎么快速加索引?
核心思路:优先用 pt-online-schema-change / gh-ost 做在线索引,若无工具则新表分批迁移并 swap,注意控制 chunk 并监控复制滞后与 IO。
目标:在线/低影响地为大表建立索引,避免锁表、宕机或过长的复制延迟。
实操思路(由易到稳):
1.优先评估
确认需要的索引列、是否为单列或复合索引、是否能构成覆盖索引。
查看表行格式、主键、已有索引、数据量与写入速率。
SHOW CREATE TABLE t; SHOW INDEX FROM t; SELECT COUNT(*) FROM t;
2.使用在线 DDL 工具(推荐)
如果是 MySQL 5.6+ 或 Percona/MyRocks,有在线 DDL:ALTER TABLE ... ADD INDEX ...(某些情况下会做 inplace)
更稳妥:使用 pt-online-schema-change 或 gh-ost(非阻塞、逐行复制、触发器/同步方式):
pt-online-schema-change --alter "ADD INDEX idx_col(col)" D=db,t=table --execute
gh-ost --alter "ADD INDEX idx_col(col)" --allow-on-master
3.分步骤做(如果不能用在线工具)
建一张新表 table_new(带新索引),使用批量 INSERT SELECT 分批迁移(每次 LIMIT/WHERE 分段),验证后 swap 表名(需要短小锁)。
或者采用分区策略先建分区后逐个分区添加索引(视 MySQL 版本而定)。
4.控制并发与监控
在迁移/在线DDL期间限制并发写(流量窗口、节流),监控 replication lag、IO、TPS、慢查询。
在 pt-online-schema-change 可配置 --max-load、--critical-load、--chunk-size 等参数。
风险与注意:
直接 ALTER TABLE 在旧版本会复制表并锁表,慎用。
索引会增加写放大(每次写要维护索引),评估写负载是否能承受。
若索引列选择性差,添加索引收益有限。
2.如果项目上有导出 Excel 很慢,如何优化?
核心思路:把导出改为异步任务 + 流式分批读取 DB + 写到对象存储供下载,并用只读副本/预聚合表避免对 OLTP 影响。
导出慢通常因 DB 查询慢、数据量大、单线程写文件、网络/IO 瓶颈。
优化策略(实战步骤):
1.诊断瓶颈
是查询慢?EXPLAIN 查看。还是组装/写文件慢?还是上传/下载慢?监控 CPU、IO、网络。
2.分批流式导出(避免一次性读入内存)
服务器端使用分页或 cursor(JDBC ResultSet streaming、MyBatis fetchSize)逐行写入文件流,避免全部加载。
示例(Java):statement.setFetchSize(1000); ResultSet rs = stmt.executeQuery(); while(rs.next()) writeRow(rs);
3.后台异步导出 + 预生成 + 分块压缩
不做实时导出:用户提交导出任务,后台异步执行、分块写 CSV/XLSX 到 S3,并在完成后通知下载。
分块并行写,然后合并或提供分段下载。
4.使用更轻量的格式
大数据导出用 CSV(流式写)而不是 Excel xlsx(内存占用大)。只有小量数据提供 xlsx。
5.数据库层优化
优化 SQL(索引/投影列减少)、使用物化视图或预聚合表,避免复杂 join/计算在导出时执行。
对导出字段做列裁剪(只查询必须字段)。
6.并行化与资源隔离
将导出任务放到专用集群或限流(避免占用主库资源)。可在只读副本上跑导出查询。
7.文件生成优化
使用高效的写库(Streaming API、SAX-like writer),避免 DOM 风格全部加载。
压缩传输(gzip),分块上传以减少内存压力。
3.大表场景下不做分库分表的优化手段(覆盖索引、分区、物化视图)
当无法拆库拆表时,常用优化技巧:
1.覆盖索引(Covering Index)
创建索引包含查询所需的所有列,查询仅扫描索引无需回表:CREATE INDEX idx ON t(col1, col2, col3),SELECT col1,col2 FROM t WHERE ...。
好处:减少 IO,提升查询速度。
2.表分区(Partition)
按 range/hash/list 分区(MySQL 分区表),可以把查询限制到部分分区,减少扫描量。
例如按日期 PARTITION BY RANGE (to_days(created_at)).
3.物化视图 / 预聚合表
频繁的聚合查询维护一个定期或增量更新的物化表,查询直接读物化数据。
可结合触发器/异步任务维护(Outbox/CDC+ETL)。
4.只读副本 / 读写分离
导出/报表/大查询走只读副本减轻主库压力。注意复制延迟问题。
5.按需索引与覆盖查询
只索引常用查询列,合适使用复合索引顺序,避免过多小索引增加写成本。
6.SQL 重写与拆分
将复杂查询拆成小查询组合、使用临时表、分段聚合以降低单次工作量。
7.缓存/近实时缓存层
热点数据缓存于 Redis 或物化查询缓存;使用 TTL 与主动/惰性失效策略。
8.Batch & Bulk 操作
批量写入和批量读取以提高吞吐,减少锁与网络开销。
注意事项:这些方法能显著缓解但不能完全替代分库分表;写放大、维护成本与一致性问题要评估。
5.ArrayList 扩容触发时的影响与优化
问题回顾:ArrayList 在扩容(通常 newCapacity = oldCapacity + oldCapacity >> 1)时会分配更大数组并复制元素,可能导致瞬时内存与 CPU 消耗。
影响:
GC 与内存短期峰值:分配新数组会多占一份内存,旧数组随后被回收,触发 GC。
复制开销:System.arraycopy 成本(O(n))导致延迟,尤其在大数组或高并发情况下影响响应。
并发问题:ArrayList 非线程安全,扩容时并发写可能丢数据或抛异常。
优化手段:
1.预分配容量
如果能预估大小,使用 new ArrayList<>(expectedSize) 避免扩容复制。
2.使用合适的数据结构
高并发场景用 CopyOnWriteArrayList(读多写少)或 ConcurrentLinkedQueue/LinkedBlockingQueue(并发写)。
频繁插删除可用 LinkedList 或其他结构避免频繁复制(取舍空间/时间)。
3.批量追加
使用 ensureCapacity() 或 addAll(Collection) 以减少扩容次数。
4.避免单请求构造超大集合
流式处理 / 分块处理,避免一次性加载巨量数据到内存。
面试一句话:扩容代价是 O(n) 的复制与短时内存峰值,预分配或流式/分块处理能避免。
6.JVM 写入文件到磁盘过程 / IO 性能考虑
核心思路:Java 写文件是应用→内核 page cache→异步写盘,IO 优化侧重合并写、缓冲、批量 fsync 策略与异步/零拷贝方法。
简要流程(Java 应用层到磁盘):
1.应用写入用户空间缓冲(JVM/OS 用户态缓冲,如 BufferedOutputStream)。
2.内核态缓冲(page cache):write() 系统调用将数据拷贝到 page cache(不立即落盘)。
3.内核异步刷新:内核在合适时机调用 pdflush/flush 将 page cache 刷入磁盘(写回)。
4.fsync (如果调用):显式调用 fsync 会确保数据落盘(阻塞直到磁盘确认)。
5.磁盘控制器与硬件:磁盘有自己的缓存与控制器,最终磁盘介质写入耗时由设备决定。
性能优化点:
避免频繁 fsync:默认使用异步写可高性能,但若需要强一致必须 fsync。可以批量 fsync 或使用 write-behind。
使用缓冲流:BufferedOutputStream、NIO ByteBuffer、直接缓冲区(DirectByteBuffer)减少复制。
批量写/合并小 IO:合并小写操作成较大写入,减少 syscalls。
使用异步/零拷贝 IO:NIO FileChannel.transferTo/transferFrom 可做零拷贝,适合大文件传输。
合理线程/队列设计:后台写线程池,生产者写入队列,消费者批量 flush。
磁盘与文件系统调优:RAID/SSD 优化、文件系统 mount 参数(noatime/nodiratime)、调整 vm.dirty_ratio/vm.dirty_background_ratio。
使用对象存储:大文件写入优先发到 S3/OSS,避免本地磁盘瓶颈与持久化复杂性(适合导出场景)。
7.CDN 流量异常的原因与解决(缓存/回源压力)-待完善
8.如何快速、安全地将 1000 亿条数据插入(批量/并发/分区/外部存储)
场景极端,要设计批量导入流水线,保证速度与可恢复性。
总体思路(分层):
1.分片与并行
把数据按某键(hash / 范围 / 时间)分区,分发到多个并行任务/实例插入,每个任务只处理自己分片。
2.批量写(批量大小调优)
使用批量插入 INSERT ... VALUES (...),(...),(...) 或 DB 批量 API,减少每条产生的开销。
选择合适 batch size(例如 1k–10k),避免单次事务过大或网络包过大。
3.临时禁用索引与约束(插入后再建)
如果可能:在批量加载时禁止二级索引/触发器/外键校验,插入完成后一次性建索引(更快)。但注意可行性与一致性风险。
4.使用分区表 / 多库多表
数据库层分区/分表可以并行写入到不同文件/磁盘,减少争用。
5.使用更适合批量写入的存储
用列式/大数据系统(Hadoop HDFS、Hive、Parquet、ClickHouse、Cassandra)或对象存储(S3)和后续 ETL,而不是单个关系型 DB。
6.流式导入 + 并行消费者
将数据放入消息系统(Kafka),消费者并行批量写入;支持重试与回溯。
7.幂等与断点续传
每条记录带唯一 ID,插入幂等(ON DUPLICATE KEY UPDATE 或去重表),支持任务失败后重跑。
8.硬件与网络优化
使用高 IO 实例、SSD、网络带宽、将数据拷贝到目标机附近(同机房),减少跨机房流量。
9.监控与回滚策略
监控写入速率、延迟、错误率;确保失败数据可单独重试或回滚。
示例流程(可落地):
将原始数据切成 N 个分片(按 hash),每个分片由一组并发 worker 读取并做批量写入。
在写入 DB 前把数据先写成 Parquet 到 S3(可并行),再用数据库或 ETL 工具(Spark/Presto)批量导入/转换。
若必须写入 MySQL:先写到中间分布式写入层(如 TiDB/TiKV/Cockroach),或用分布式 Loader(mydumper/loader 风格),并最终建立索引。
面试一句话:把问题拆成分片并行 + 批量小事务 + 禁止索引/延后建索引 + 使用适合的存储(对象存储/列式/大数据引擎)是关键。