后端性能优化方法论
一、性能理论基础
性能优化不是凭感觉调参数,而是基于指标体系、数学模型和科学方法论的工程实践。脱离理论的优化只是在碰运气。
1.1 核心性能指标
| 指标 | 定义 | 说明 |
|---|---|---|
| QPS (Queries Per Second) | 每秒查询数 | 衡量系统吞吐能力,服务端视角:”系统每秒能处理多少请求” |
| TPS (Transactions Per Second) | 每秒事务数 | 衡量业务处理能力,一个事务可能包含多个查询;压测时 TPS 比 QPS 更能反映真实业务能力 |
| RT (Response Time) | 响应时间 | 从客户端发出请求到收到完整响应的时间,含网络传输、排队、处理 |
| P50/P90/P99/P999 | 分位数延迟 | P99 = 99% 的请求延迟在此值以下;均值掩盖真相,分位数暴露长尾 |
| 并发数 | 同时处理的请求数 | 狭义:系统内正在处理中的请求数;广义:同时连到系统的用户/连接数 |
| 错误率 | Error Rate | 失败的请求占比;目标通常 < 0.01%(4 个 9),核心支付链路要求更高 |
为什么平均值具有欺骗性? 假设 100 个请求,99 个在 10ms 内完成,1 个耗时 10s。平均 RT ≈ 110ms——看起来还行,但实际上有 1% 的用户等了 10 秒。P99 = 10s 才是真实用户体验。
1 | 请求耗时分布示例(单位 ms): |
1.2 吞吐量与延迟的关系
很多人以为:吞吐量上升 → 延迟也上升。这不总是对的。 实际关系有三个阶段:
1 | 延迟 |
三条线的本质差异:
- 空闲区:请求几乎无需排队,RT ≈ 服务处理时间。增加并发,吞吐线性增长,延迟几乎不变。
- 线性增长区:CPU/线程开始排队,RT 包含排队时间。吞吐继续增长但斜率下降。
- 过饱和区:某个资源(CPU/线程池/连接池/DB)达到瓶颈,排队无限堆积,RT 急剧恶化,吞吐不升反降。
核心结论:你在压测时要找到”线性增长区 → 过饱和区”的拐点,那就是系统的最大安全吞吐量,线上流量应控制在此拐点的 60%-70% 以内。
1.3 Little’s Law
Little’s Law 是排队论中最简单也最强大的公式:
1 | L = λ × W |
实际应用:
- 容量推导:已知目标 QPS = 1000,RT ≤ 100ms,需要多少线程?L = 1000 × 0.1s = 100。因此线程池至少需要 100 个线程。
- 瓶颈反向推断:线上 P99 RT = 200ms,QPS 峰值 = 5000,则峰值并发 L = 5000 × 0.2s = 1000。如果线程池只有 200 个线程,请求必然排队积压——不是代码慢,是池太小。
- Tomcat 线程池规划:
maxThreads应 ≥ L_peak(峰值并发数),否则请求在 OS accept queue 排队。
1 | 实际计算示例: |
1.4 Amdahl’s Law 与 Gustafson’s Law
Amdahl’s Law(阿姆达尔定律)——并行加速的极限由串行部分决定:
1 | Speedup = 1 / (S + (1 - S) / N) |
这是悲观主义者的定律——它告诉你加再多机器也突破不了串行瓶颈。在微服务架构中,一个需要串行调用 5 个下游依赖的接口,即使每个下游都完美水平扩展,串行的调用链本身也会成为瓶颈。
Gustafson’s Law(古斯塔夫森定律)——大规模问题可获近乎线性加速:
1 | Scaled Speedup = S + N × (1 - S) |
两个定律不是矛盾的,而是回答了不同问题:
- Amdahl 回答:固定问题规模,加机器能快多少?(悲观)
- Gustafson 回答:机器越多,能做多大的问题?(乐观)
性能优化的启示:
- 识别串行瓶颈比增加并行度更重要——先做 benchmark 找到 Amdahl 串行部分
- 消除全局锁、单点串行化、同步等待是提升水平扩展能力的前提
- 如果一个接口串行调用了 8 个下游,即使每个都很快(10ms),总 RT 也至少 80ms——考虑用
CompletableFuture并行化或Reactive异步合并
1.5 性能测试金字塔
1 | /\ |
| 层级 | 测试类型 | 目标 | 典型问题 |
|---|---|---|---|
| L1 | 基准测试 | 建立性能基线 | microbenchmark (JMH);单接口/单 SQL 的最佳性能 |
| L2 | 负载测试 | 验证预期负载下的行为 | 给定 QPS 下 RT/错误率是否达标 |
| L3 | 压力测试 | 找到系统极限拐点 | 持续加压直到 RT 飙升/错误率突破——找到最大吞吐 |
| L4 | 稳定性测试 | 验证长时间运行不退化 | 内存泄漏、连接泄漏、GC 恶化、日志膨胀 |
| L5 | 容量测试 | 推算线上极限容量 | 单机上限 → N 台集群上限;规划扩容时机 |
压测前置条件:
- 独立压测环境(非生产),但配置与生产一致
- 压测数据量要与生产同量级(索引行为/缓存命中率对数据量敏感)
- 消除单测干扰——压测期间排除定时任务、其他批处理
- 预热完成后再采样(JIT 编译、缓存、连接池初始化均需要时间)
二、性能分析方法论
调优之前先定位。盲目修改配置是性能优化最忌讳的事——没有数据支撑的优化都是玄学。
2.1 CPU 密集型 vs IO 密集型识别
大部分性能问题的根因都可以归为两类:
| 类型 | CPU 密集型 | IO 密集型 |
|---|---|---|
| 特征 | CPU 使用率持续 > 80% | CPU 使用率低,大量线程处于 Waiting/Blocked 状态 |
| 典型表现 | GC 频繁、序列化开销大、计算逻辑重 | 数据库/Redis/RPC 调用慢,连接池耗尽等待 |
| 定位命令 | top -H, perf top, 火焰图 |
thread dump, 连接池监控, Arthas trace |
| 优化方向 | 减少对象创建、优化算法、并行计算、GC 调优 | 批量操作、异步化、连接池优化、缓存 |
快速判断命令(Linux):
1 | # CPU 使用率 |
2.2 自顶向下法(Top-Down)
不要上来就翻代码。正确的排查顺序是 宏观 → 微观,避免”拿着放大镜找问题”。
1 | 第1层:业务指标 ← 哪一个接口慢?多慢?QPS 多少? |
实例:线上某个接口 P99 从 50ms 恶化到 500ms。
1 | L1: 监控发现 /api/order/create P99=500ms, QPS 无明显变化 |
2.3 USE 方法(Utilization/Saturation/Errors)
Brendan Gregg 提出的 USE 方法,适用于分析任何系统资源——CPU、内存、磁盘、网络、连接池等。
对每种资源,回答三个问题:
| 维度 | 含义 | CPU 示例 |
|---|---|---|
| 利用率 (Utilization) | 资源忙碌的时间比例 | CPU 使用率 85%(可能有问题) |
| 饱和度 (Saturation) | 排队等待的工作量 | 运行队列长度 (load average) 持续 > CPU 核数 |
| 错误 (Errors) | 错误事件的数量 | top 中 %iowait 持续 > 10% 可能会引发 IO 错误;应用层 5xx 错误 |
1 | USE 检查清单(每次排查问题必过一遍): |
2.4 火焰图分析
火焰图是定位 CPU 热点和阻塞点的终极工具。它不是看”某个方法耗时多少”,而是可视化所有正在执行的函数调用栈。
On-CPU 火焰图:回答”CPU 时间花在哪了”。适用于 CPU 使用率高的情况。
1 | 顶部宽的平台 → 该函数占用 CPU 多 |
Off-CPU 火焰图:回答”线程在等什么”。适用于 CPU 不高但 RT 高的情况。
1 | 线程不在 上运行的原因 → 等锁、等 IO、等网络 |
Java 火焰图获取:直接 perf 拿到的栈帧是 JIT 编译后的,需要配合 perf-map-agent 映射回 Java 方法名。更简单的方式是用 async-profiler:
1 | # async-profiler 开箱即用,自动解析 Java 方法名 |
火焰图阅读技巧:
- 宽度代表耗时/采样次数,关注最宽的平台
- 先看顶层的宽平台(叶子函数),再看调用链
- 出现大量
pthread_cond_wait/epoll_wait/futex→ IO 等待 - 出现大量
malloc/memcpy→ 内存操作密集
2.5 JFR + JMC 分析
Java Flight Recorder (JFR) 是 JVM 内置的低开销(< 2%)性能分析工具,数据以二进制 .jfr 文件保存,用 JDK 自带的 JMC (Java Mission Control) 打开分析。
JFR 能回答的关键问题:
- 哪个方法分配了最多内存?→ Memory → Allocations
- 锁竞争发生在哪里?→ Java Application → Lock Instances
- GC 各阶段耗时?→ 内存 → GC
- IO 操作耗时分布?→ I/O → File Read/Write, Socket Read/Write
- 线程阻塞在什么操作上?→ Threads → Thread Dump
1 | # 启动时开启 JFR |
JFR vs async-profiler 选型:
- JFR:适合生产环境长期监控,开销极低,可连续数小时录制
- async-profiler:适合开发/测试环境深度分析,火焰图更直观,可追踪内核级调用栈
2.6 Arthas 在线诊断
Arthas(阿尔萨斯)是阿里开源的 Java 在线诊断工具,无需重启应用即可追查线上问题。它是排查非可复现线上性能问题的利器。
1 | # 启动 |
六大核心命令:
| 命令 | 功能 | 典型场景 |
|---|---|---|
| dashboard | 实时面板:线程/内存/GC/Runtime | 第一眼:现在系统状态如何? |
| thread | 线程分析 | thread -n 3 看 top 3 CPU 线程;thread -b 找死锁 |
| trace | 方法调用链路+耗时 | trace com.x.Service method -n 5 看调了什么、慢在哪 |
| watch | 方法入参/返回值/异常 | watch com.x.Service method '{params,returnObj,throwExp}' -x 3 |
| vmtool | 强制 GC、查看内存 | vmtool --action forceGc |
| jad | 在线反编译 | jad com.x.Service 确认线上运行的代码版本 |
核心排查套路(线上接口慢):
1 | # 1. 看全局 |
1 | # ===== 核心参数 ===== |
G1 调优实战流程:
1 | 1. 设 MaxGCPauseMillis = 100(初始值) |
3.4 ZGC
ZGC 是 JDK 11 引入的低延迟垃圾收集器,最大特点是暂停时间与堆大小无关(JDK 17+ 版本保证 < 1ms)。
1 | -XX:+UseZGC # 启用 ZGC |
ZGC 的核心优势:
- 暂停时间 < 1ms(不随堆大小增长)
- 支持 TB 级堆(生产环境有 16TB 堆的案例)
- 染色指针技术,通过指针中的 metadata bit 实现并发整理
ZGC 调优要点(虽然它”基本无需调参”,但仍需留意):
- 给堆留足余量:并发回收期间不停止应用,回收速度跟不上分配速度会导致 Allocation Stall
- 设置
-XX:ZAllocationSpikeTolerance(分配尖峰容忍度),默认 2.0 - 观察
-Xlog:gc+zgc*=info日志中的 Allocation Stall 事件
3.5 堆大小规划
基本原则:
-Xms和-Xmx设为相同值,避免 heap resize 带来的 GC 开销- 堆大小不超过物理内存的 50%-75%(给 OS page cache 和 Metaspace 留余地)
- 容器化环境:Java 10+ 支持
-XX:MaxRAMPercentage替代固定-Xmx
1 | # 物理机/VM 部署(16G 内存) |
堆大小规划公式:
1 | 堆大小 ≥ 存活数据大小 × 1.5 ~ 2.0 + 每次请求临时对象 × 并发数 |
3.6 元空间与直接内存
1 | # 元空间(Metaspace)——存储类元数据 |
元空间 OOM 常见原因:
- 动态代理/反射大量生成类(CGLIB、动态代理、Lambda)
- Groovy 脚本热加载(类泄漏)
- 排查:
jcmd <pid> VM.classloader_stats查看各 ClassLoader 加载的类数量
3.7 常见 GC 问题诊断
| 症状 | 根因 | 解决方案 |
|---|---|---|
| 频繁 YGC (< 1 秒/次) | 年轻代太小或分配速率太高 | 增大年轻代 (-XX:G1NewSizePercent) 或减少代码中临时对象 |
| YGC 暂停时间长 (> 200ms) | Survivor 区溢出或对象复制开销大 | 增大 Survivor 空间或调整 -XX:SurvivorRatio |
| 连续 Full GC | (1) System.gc() 被调用、(2) 大对象分配失败、(3) 元空间不足、(4) 老年代碎片 | (1) -XX:+DisableExplicitGC、(2) 增大 -XX:G1HeapRegionSize、(3) 增大 Metaspace、(4) 增大堆或排查大对象 |
| Promotion Failed | Old 区碎片化,无法找到连续空间容纳晋升对象 | 调整 IHOP 或增大堆 |
| GC 吞吐低于 95% | GC 太频繁 → 堆太小 | 增大堆 或 排查代码中过多的临时对象创建 |
G1 Humongous Object 问题:超过 Region 大小 50% 的对象直接分配到老年代的 Humongous Region。如果频繁分配大对象,Region 碎片化严重会触发 Full GC。排查:-XX:+PrintAdaptiveSizePolicy 配合 GC 日志查看 Humongous allocation 次数。
3.8 内存泄漏排查流程
1 | 步骤1:获取 Heap Dump |
四、Java 代码级优化
GC 调优解决的是”如何高效回收”,代码级优化解决的是”为什么产生这么多垃圾”。
4.1 对象生命周期管理
不要为了”优雅”而过度创建对象。每个 new 都是一次堆分配,分配压力大 → YGC 频繁 → RT 抖动。
1 | // ❌ 每个请求都创建 Calendar/DateFormat |
对象池 vs 垃圾回收:
- 轻量级对象(生命周期短):让 GC 处理更高效
- 重量级对象(线程、连接):池化是必须的
- 中间地带(ByteBuffer、StringBuilder):连接复用或 ThreadLocal 可能优于反复创建
4.2 String 优化
String 是 Java 中最常用的不可变对象,也是 GC 压力的重要来源。
1 | // ❌ 循环内字符串拼接:每次迭代创建新的 StringBuilder + String |
字符串常量池:String.intern() 可以将动态生成的字符串放入常量池,减少重复字符串的内存占用。但需要权衡——intern 操作需要锁 StringTable(HotSpot 中为哈希表),并发调用开销不小。
G1 字符串去重(JDK 8u20+):
1 | -XX:+UseStringDeduplication # 自动去重相同内容的 char[] 底层数组 |
4.3 集合优化
指定初始容量,避免扩容带来的数组复制和中间垃圾:
1 | // ❌ 默认容量 16 → 扩容 16→32→64→128→256... 每次扩容都复制 + 丢弃旧数组 |
遍历效率对比(100 万元素循环,平均耗时):
1 | 传统 for (ArrayList, 按索引) → 最快(无额外开销) |
结论:性能敏感的热点路径用传统 for 或 for-each,Stream 用于复杂数据转换(filter/map/reduce)场景而非纯遍历。
4.4 序列化优化
| 序列化方案 | 速度 | 体积 | 跨语言 | 适用场景 |
|---|---|---|---|---|
| JSON (Jackson) | ★★★ | ★★★ | ✓ | 通用 API,对前端友好 |
| Protobuf | ★★★★★ | ★★★★★ | ✓ | 内部服务通信,高吞吐 |
| Kryo | ★★★★★ | ★★★★★ | ✗ | 纯 Java 环境(Dubbo/Spark) |
| Java Serializable | ★ | ★ | ✗ | 仅限 Legacy 兼容 |
实际数据对比(10 万条订单对象):
1 | JSON: 18.7 MB, 序列化 450ms, 反序列化 780ms |
选择序列化方案时还要考虑长连接策略——Protobuf 的二进制格式天然支持基于长连接的流式传输(如 gRPC),减少 TCP 握手开销。
4.5 并发优化
减少锁粒度:
1 | // ❌ 粗粒度锁——整个方法串行化 |
锁选型决策树:
1 | 需要加锁保护的数据操作: |
避免伪共享(False Sharing):
1 | // CPU 缓存行通常 64 字节。两个变量在同一缓存行时,一个线程写 → 另一个线程的缓存行失效 |
4.6 缓存预热与缓存穿透
缓存预热:服务启动后、上线前,预先加载热点数据到缓存(本地 Caffeine/Guava Cache 或 Redis)。避免上线瞬间流量直接打到数据库。
1 |
|
缓存穿透防护:大量请求查询一个不存在的数据(DB 也没有),缓存不命中,请求全打到 DB上。
解法:
- 布隆过滤器(Bloom Filter):用极小的内存判定 key 是否”可能存在”
- 缓存空值:DB 返回 null 时,缓存一个短期空对象(TTL 短,如 1min)
- 请求合并:同一 key 的并发查询只放一个到 DB,其余等待首个完成
五、数据库优化
详细的 MySQL 底层原理请参阅 MySQL 深度解析,本节聚焦性能优化方法论。
5.1 SQL 优化策略
SQL 优化是性价比最高的优化手段。一条慢 SQL 可能吃掉 80% 的 DB CPU。
索引优化是 SQL 调优的起点(详见 MySQL 文章 第六章):
- 覆盖索引:查询列完全在索引中,避免回表
- 最左前缀原则:联合索引
(a, b, c)的过滤条件必须从 a 开始 - 避免索引失效:函数运算
WHERE DATE(create_time) = '2026-01-01'、隐式类型转换WHERE phone = 13800138000(phone 是 varchar)、前置模糊LIKE '%keyword' - 索引下推(ICP, Index Condition Pushdown):MySQL 5.6+ 在索引层过滤,减少回表
Join 优化:
- 小表驱动大表(小表作为驱动表,减少被驱动表循环次数)
- Join 字段必须有索引(Nested-Loop Join 中被驱动表走索引)
- 超过 3 表的 Join 考虑拆分——在应用层组装或用临时表
子查询改写:
1 | -- ❌ 相关子查询:外表每行都执行一次子查询 |
分页优化(深分页问题):
1 | -- ❌ 偏移量 100000 → 扫描并丢弃前 100000 行 |
5.2 连接池优化(HikariCP)
HikariCP 是 Spring Boot 2.x 默认连接池,以性能著称。优化不在调参数,在合理的池大小。
连接池大小计算公式:
1 | maximumPoolSize = ((core_count * 2) + effective_spindle_count) |
详细公式推导和连接池原理参见 MySQL 深度解析 第五章 连接管理与线程池。
关键参数:
1 | spring.datasource.hikari: |
5.3 读写分离与分库分表
读写分离:一主多从,写走主库,读走从库。适合读多写少的场景。
1 | ┌── 主库 (读写) ──┐ |
详解参见 MySQL 深度解析 第九章 高可用架构。
分库分表:水平拆分解决单库单表的写入瓶颈。本质上用”空间(更多机器)”换”写入吞吐”。
| 拆分维度 | 策略 | 适用场景 |
|---|---|---|
| 按 user_id 取模 | hash(user_id) % N | 用户体系均匀分布 |
| 按时间 | 按月/年分表 | 日志/流水类数据 |
| 按地市/业务线 | 按业务维度分库 | 多租户 SaaS |
详细分片策略、分布式 ID 生成、跨片查询问题参见 MySQL 深度解析 第十章 分库分表实战。
5.4 批量操作
批量插入是优化写入吞吐最直接的手段——N 次网络往返合并为 1 次。
1 | -- ❌ 逐条插入(10000 条 = 10000 次网络往返 = 10000 次事务) |
JDBC 层面使用 rewriteBatchedStatements=true(MySQL 驱动),否则每条 SQL 仍是一次独立发送。
1 | spring.datasource.hikari.data-source-properties: |
批量更新同理,使用 JDBC batch API 或框架提供的批量更新能力(MyBatis BatchExecutor、JdbcTemplate.batchUpdate)。
5.5 慢查询分析
1 | -- 开启慢查询日志(生产建议开启) |
线上紧急排查(不靠慢查询日志):
1 | -- 查看当前正在执行的所有 SQL(执行中 > 5 秒的) |
六、中间件优化
6.1 Redis 优化
详细的数据结构底层原理和高可用架构请参阅 Redis 深度解析,本节聚焦性能调优。
Pipeline 批处理:将多次命令的 RTT(Round Trip Time)合并为一次网络往返。
1 | // ❌ 逐条执行:每次 RTT = 1ms,1000 条 = 1000ms |
详细命令的复杂度分析和高效使用参见 Redis 深度解析 第三章 命令高效应用。
大 Key / 热 Key 处理:
| 问题 | 影响 | 解决方案 |
|---|---|---|
| 大 Key (String > 10KB, 集合 > 10000 元素) | 读取阻塞单线程,删除耗时长,迁移困难 | 拆分大 Key(大 Hash 按域拆分为多个小 Hash);集合元素改为分页读取;删除用 UNLINK(异步删除) |
| 热 Key (单 Key 的 QPS > 5000) | 单分片 CPU 打满,导致局部热点 | 多副本(客户端缓存本地备份);前綴拆分(key_1, key_2, … key_n);Proxy 层 Hot Key 探测+自动副本 |
连接池优化(Lettuce):
1 | spring.redis.lettuce.pool: |
详细持久化(RDB/AOF)及主从架构的可靠性影响参见 Redis 深度解析 第六章 持久化与高可用。
6.2 MQ 优化
详细的技术选型和消息可靠性保证请参阅 消息队列,本节聚焦性能优化。
批量发送:消息不逐条投递,而是积攒到一定数量或时间后批量发送,显著提升吞吐。
| MQ | 批量发送方式 | 配置要点 |
|---|---|---|
| RocketMQ | DefaultMQProducer.send(messages) 批量 List |
同批次消息 Topic 必须相同;批次总大小 < 4MB |
| Kafka | linger.ms + batch.size |
linger.ms=5(等待 5ms 攒批),batch.size=16384(16KB 批次) |
| RabbitMQ | 暂无原生批量 API,通过 Publisher Confirms + 批量 confirm 模拟 |
使用 BatchingRabbitTemplate(Spring AMQP 封装) |
MQ 的消息可靠性三个维度(不丢/不重/有序)详见 消息队列 第五章 消息可靠性。
消费优化:
1 | // ❌ 逐条消费 + 逐条提交(offset/ACK) |
消息压缩:对传输中的消息体进行压缩,减少网络带宽和存储开销。
1 | # RocketMQ |
6.3 Elasticsearch 优化
| 优化维度 | 策略 | 具体做法 |
|---|---|---|
| 索引分片 | 分片数 = 节点数的 1.5~3 倍 | 单分片 10-50GB 为最佳实践;避免分片过多(集群管理开销大) |
| Refresh 频率 | 降低刷新频率 | index.refresh_interval: 30s(默认 1s,写入密集场景可调大或关闭) |
| Bulk 写入 | 批量写入 + 关闭 refresh | 先关 refresh → 批量写入 → 开 refresh;批量大小 5-15MB |
| 查询优化 | 避免深分页 | from + size 深度 > 10000 时改用 search_after;避免全量 size: 0 后用聚合 |
| Mapping | 关闭不需要的索引功能 | 不需要全文检索 → "index": false;不需要评分 → "norms": false |
| Translog | 异步持久化 | index.translog.durability: async(少量写入可靠性换取巨大吞吐提升) |
6.4 Nginx 优化
1 | # worker 配置 |
七、系统架构优化
如果前面的微观调优是”修修补补”,架构优化就是”改设计”。有些性能问题从代码层面无解,必须在架构层面重构。
7.1 多级缓存
1 | ┌─────────────────────────────────────────────────────────┐ |
一致性策略:
- Cache Aside(旁路缓存):读 → 缓存未命中 → 读 DB → 写缓存;写 → 更新 DB → 删缓存。最常用。
- Write Through:写 → 同步写缓存 + DB。一致性最强但写入延迟高。
- Write Behind:写 → 写缓存 → 异步批量写 DB。性能最好但存在数据丢失窗口。
热点探测与缓存预热:在本地缓存中维护热点 Key 统计,高 QPS 的 Key 标记为热点,提前预热到 L1 甚至预加载到入站请求队列中。
7.2 异步化
MQ 解耦是最经典的异步手段。非核心链路从同步调用剥离为异步消息,参见 6.2 节及 消息队列。
CompletableFuture 并行化:一个接口需要同时调用多个下游服务时,串行调用总 RT = 各服务 RT 之和;并行调用总 RT = max(各服务 RT)。
1 | // ❌ 串行调用:200ms + 300ms + 150ms = 650ms |
响应式编程(Reactive Programming / WebFlux / Project Loom Virtual Thread):
- 适用场景:高并发 + IO 密集型(网关、消息推送、实时流处理)
- 核心逻辑:用少量线程处理大量并发请求,线程不被阻塞在 IO 等待上
- 代价:开发复杂性高,调试困难,代码风格不直观
- Loom 虚拟线程(JDK 21+)让传统阻塞代码获得近似响应式的可伸缩性
7.3 批处理与合并
请求合并:对高并发下请求相同资源的场景,将多个请求合并为一次后端查询。
1 | 场景:1000 个请求同时查询同一个 productId 的商品信息 |
批量操作:见 5.4 和 6.1,将逐条操作合并为批量是通用优化模式。
7.4 池化技术
| 池类型 | 调优要点 | 常见问题 |
|---|---|---|
| 线程池 | coreSize = 峰值并发 × 1.3;maxSize 可稍大;队列选 LinkedBlockingQueue(无界慎用)或 ArrayBlockingQueue(有界+拒绝策略) |
拒绝策略选 CallerRunsPolicy(让调用线程执行,天然限流)或 DiscardOldestPolicy |
| DB 连接池 | 见 5.2 | 连接泄漏(未 close)、连接过多(DB 拒绝) |
| Redis 连接池 | 见 6.1 | Lettuce 单连接多路复用,通常无需大池 |
| HTTP 连接池 | 连接池大小 = 目标服务实例数 × 每个实例的最大连接数;maxPerRoute < 目标服务 accept 上限 |
连接池耗尽 → 请求排队 → 雪崩 |
7.5 预计算
空间换时间的典型案例:
- 电商首页分类聚合:定时任务每天凌晨预计算 Top 类目 → 缓存中直接取,而非每次请求实时聚合
- 用户未读消息数:写入消息时同步更新 counters 表,而非每次查询时
COUNT(*) - 报表/大屏:T+1 预聚合到指标表,实时要求不高的数据不走实时 SQL
- 搜索推荐索引:提前构建倒排索引(Elasticsearch 本质上就是空间换时间)
7.6 读写分离与 COW(Copy on Write)
读写分离(见 5.3)本质是用冗余(从库)换读取并发能力。同样思路适用于应用层:
- 一份数据的”读写版本”分离——读走快照副本,写操作在后台生成新版本
- COW:对于读多写少的配置数据,更新时复制一份新数据,修改完成后原子替换引用(
volatile或AtomicReference)
1 | // COW 实现配置热更新 |
7.7 数据结构选择
数据结构的选择直接影响算法复杂度,不同场景的容器选型能带来数量级的性能差异:
| 场景 | 推荐结构 | 时间复杂度 | 避免使用的结构 |
|---|---|---|---|
| 频繁按 key 查找 | HashMap |
O(1) | ArrayList 线性查找 O(n) |
| 需要有序查找 | TreeMap |
O(log n) | ArrayList 按 key 查找后再排序 O(n + n log n) |
| 去重 + 快速 contains | HashSet |
O(1) | ArrayList.contains() → O(n) |
| 最值/排行榜 | PriorityQueue 或 TreeSet |
O(log n) | 每次 Collections.sort() → O(n log n) |
| 前缀/模糊匹配 | Trie(前缀树) | O(k), k=长度 | 遍历 + startsWith 每个 O(n) |
八、压测实战
8.1 压测工具选型
| 工具 | 类型 | 特点 | 适用场景 |
|---|---|---|---|
| JMH | 微基准测试 | JVM 级微基准,控制 JIT 预热/死代码消除 | 单方法/单算法性能对比 |
| wrk / wrk2 | HTTP 压测 | Lua 脚本可编程,吞吐极高 | 单接口压测,快速获取系统上限 |
| ab (Apache Bench) | HTTP 压测 | 简单,无脚本能力 | 快速验证,功能有限 |
| JMeter | 全链路压测 | GUI + 丰富协议支持 + 分布式 | 多接口、有依赖关系的复杂业务 |
| K6 | 现代 JS 压测 | 脚本化(JS),CI/CD 友好 | 性能回归测试,持续集成 |
| Locust | Python 压测 | Python 脚本,分布式原生 | 复杂的用户行为模拟 |
JMH 示例(基准测试一定要用 JMH,不要自己写 System.nanoTime() 循环):
1 |
|
8.2 压测流程
1 | 阶段1: 目标定义 |
8.3 全链路压测简介
全链路压测与单服务压测的本质区别:在真实的线上集群中,用构造的测试流量叠加在生产流量上,同时保证测试数据不污染真实业务数据。
核心技术挑战:
- 流量染色:测试流量打上特殊标记(RPC 透传 Header / HTTP Header),全链路传递
- 数据隔离:测试写操作路由到影子表/影子库,或使用特殊数据范围(如 uid > 某个阈值)
- 流量控制:测试流量 QPS 可实时控制,出现异常可一键终止
- 风险控制:选择低峰期,从极小流量起步,监控告警阈值调严,随时可回滚
全链路压测是大型互联网公司(阿里双十一、美团外卖日高峰)的必备能力,中小团队不必一步到位——先做好单服务压测。
8.4 容量规划
基于压测结果,推算线上容量并制定扩容节奏:
1 | 单机最大安全 QPS = 测得的 QPS_max × 0.6 (60% 余量) |
九、性能优化案例
9.1 案例一:接口从 2s 优化到 80ms
症状:订单详情接口 P99 RT = 2000ms,用户投诉页面加载慢。
排查过程:
1 | L1: 监控显示 /api/order/detail P99 = 2000ms, P50 = 500ms(长尾严重) |
优化方案:
- 并行化:8 个下游调用改为 CompletableFuture 并行 → 最长 300ms
- 缓存:商品信息、店铺信息 TTL 缓存(几乎不变的数据)→ 减少 4 个 RPC
- SQL 优化:订单明细查询改为覆盖索引,移除
ORDER BY rand()+ 无用 join
结果:P99 RT = 2000ms → 80ms(并行调用 3 个 + 缓存命中),P50 = 35ms。CPU 上涨 5%(CompletableFuture 线程开销),完全可接受。
9.2 案例二:Full GC 频繁导致定时抖动
症状:压测时 P99 RT 间歇性飙升到 5s,但 P50 只有 30ms。
排查过程:
1 | L1: GC 日志分析 → Full GC 每 2-3 分钟发生一次,每次耗时 4s+(→ P99 5s 的根因) |
优化方案:
- 为缓存加 TTL(60 分钟),过期的 POI 自动淘汰
- 改用 Caffeine 替代手动 Map 缓存(内置 LRU 淘汰策略)
- 最大缓存条数限制:
maximumSize(100000) - 补充:异步定期清理过期的 POI 引用
结果:Full GC 消失,堆内存从 7G 降至 3G 稳定,P99 ≤ 150ms。
9.3 案例三:数据库连接池耗尽
症状:每天业务高峰期接口频繁出现 Cannot get JDBC Connection,持续 5-10 分钟后自动恢复。
排查过程:
1 | L1: HikariCP 连接池监控 → active=20, idle=0, pending=50, wait=8s |
优化方案:
- 连接池扩容:
maximum-pool-size从 20 → 40(临时止血) - 报表导出改为异步:提交导出 → MQ → 异步生成文件 → 通知下载(不再同步占用连接)
- 数据库连接超时限制:
connection-timeout: 3000→ 3s 拿不到连接直接失败,不再堆积 - 报表 SQL 优化:深分页改为游标分页
结果:高峰期连接池 active 稳定从 20 → 8,等待队列消除。真正解决的是异步化改造——阻塞操作必须脱离请求主链路。
9.4 案例四:缓存热 Key 击穿
症状:秒杀开始瞬间,Redis 中商品库存 Key 过期(或有大量并发读取同一个 Key),Redis 单分片 CPU 瞬间 100%,大量请求超时,然后穿透到 MySQL 瞬间打挂。
排查过程:
1 | L1: Redis 监控:单分片 CPU 100%,QPS 集中在一个 Key(秒杀商品) |
优化方案:
- 热点 Key 前置探测:实时统计 Redis Key 的 QPS,识别热点 Key → 通知客户端本地缓存该 Key
- 互斥锁:缓存过期时,只有一个请求能去 MySQL 加载数据,其余等待
- 逻辑过期:缓存设置永不过期,value 中包含逻辑过期时间,后台线程异步刷新
- 秒杀场景特殊处理:库存扣减收口到单线程队列(如 Redis + Lua 原子操作)
结果:热点 Key 从 Redis 分片 CPU 100% → 30%,无请求穿透到 MySQL。核心经验:区分物理过期和逻辑过期,用异步刷新替代被动过期。
十、性能优化检查清单
按优先级从高到低排列。这不是一次性全部执行的列表,而是每次上线前、每次压测后、每次排查问题时应该过一遍的检查清单。
第一优先级:低风险、高收益(必做)
- SQL 慢查询日志已开启,定期 Review 并优化(目标:90% 查询 < 10ms)
- DB 连接池
maximum-pool-size经过合理计算(≤ DBmax_connections/ 服务数) - GC 日志已开启,可通过 GCEasy 分析
- JVM 堆大小已按规划设置(Xms = Xmx)
- 关键接口有缓存策略(本地缓存 + 远程缓存)
- 缓存 Key 设置了合理的 TTL(不过期也不永不过期)
第二优先级:中等风险、高收益(推荐做)
- 核心接口串行调用改为 CompletableFuture 并行化
- Redis bulk 操作使用 Pipeline,而非逐条执行
- 批量 SQL INSERT/UPDATE 使用
rewriteBatchedStatements=true - 热点路径代码消除无意义的
new对象 - 接口加入了限流/熔断/降级机制(Sentinel/Resilience4j)
- HTTP Client 配置了连接池和超时(connect/read/write timeout)
- 大查询(导出、报表)改为异步处理(MQ + 回调/轮询)
- 深分页 SQL 改为游标分页或延迟关联
第三优先级:低风险、中收益(选做)
- Nginx 开启 Gzip + 静态资源缓存 + upstream keepalive
- ES Refresh 间隔根据写入频率调整(> 30s)
- 数据库读写分离(读多写少场景)
- 建立了中央配置管理的线程池/连接池参数
- 对不可变数据使用 COW(Copy on Write)减少读锁开销
- JVM 开启
-XX:+UseStringDeduplication(G1 GC)
第四优先级:深层优化(按需)
- JFR 录制分析热点方法的对象分配
- 火焰图分析 CPU 热点函数(On-CPU / Off-CPU)
- MQ 消息压缩(LZ4/Zstd)
- Redis 大 Key / 热 Key 拆分
- 引入 Protobuf 替代 JSON 作为内部 RPC 序列化
- 使用虚拟线程(JDK 21+)改造高并发阻塞 IO 场景
- 建立全链路压测体系(中大型团队)
持续监控
- 每个核心接口的 QPS / P99 RT / 错误率 有 Dashboard
- JVM 堆内存 / GC 频率 / 线程数 / 连接池 有 Dashboard
- DB 慢查询告警 / 连接数告警 / 主从延迟告警
- Redis 内存使用率 / 慢查询 / 连接数告警
- 定期(每季度)执行一次全量压测,建立性能基线
附录:快速诊断指南
一个接口”慢”时,按以下顺序排查——这是十年来最有效的一线排查路径:
1 | 用户投诉"页面很慢" |
后记:性能优化没有银弹。最重要的不是记住哪些参数、哪些命令,而是建立一套系统化的”分析 → 定位 → 优化 → 验证”方法论。遇到任何性能问题,先问三个问题:在哪(哪个环节慢)、为什么(根因是什么)、值不值(优化的 ROI 是否合理)。
更多底层原理请参阅:
- MySQL 深度解析 — InnoDB 引擎、索引原理、事务与锁、高可用架构、分库分表实战
- Redis 深度解析 — 核心数据结构、持久化与高可用、分布式集群、缓存设计方法论
- 消息队列 — 三大 MQ 对比、消息可靠性、事务消息、最佳实践
- 可观测性 — Metrics / Logging / Tracing 三支柱落地实践