Spring Boot: 性能优化
最后更新:2026-08-26
性能优化是系统工程——JVM、数据库、连接池三个维度,精准定位瓶颈才能对症下药。
1. 你将学到
- JVM 参数调优:堆大小 / GC 算法选择(G1GC / ZGC)
- HikariCP 连接池参数优化与连接泄漏检测
- JPA 查询优化:N+1 问题与
@EntityGraph/fetch join解决方案 - 数据库索引策略与慢查询日志分析
- Alice 将 OrderFlow 订单列表 API 的 P99 延迟从 500ms 优化至 50ms
2. 一个性能工程师的真实故事
(1) 痛点:API 慢如蜗牛
Charlie 报告 OrderFlow 订单列表 API 的 P99 延迟为 500ms,用户体验极差。Alice 分析发现:1)JVM 频繁 Full GC 暂停 200ms;2)JPA 查询订单列表时产生 N+1 问题(100 条订单 = 101 条 SQL);3)HikariCP 连接池大小不够,请求等连接超时。
(2) 系统优化的解法
逐层优化,精准打击瓶颈:
| 层级 | 瓶颈 | 优化方案 |
|---|---|---|
| JVM | Full GC 暂停 | G1GC + 堆大小调优 |
| 连接池 | 连接不够用 | HikariCP 参数优化 |
| ORM | N+1 查询 | JOIN FETCH / EntityGraph |
| 数据库 | 全表扫描 | 添加索引 |
(3) 收益
Alice 逐一优化后,P99 延迟从 500ms 降到 50ms,吞吐量提升 10 倍。
3. JVM 参数调优
(1) GC 算法对比
| GC 算法 | 最大暂停时间 | 适用场景 | JVM 参数 |
|---|---|---|---|
| G1GC | 10-200ms | 通用(JDK 17 默认) | -XX:+UseG1GC |
| ZGC | < 1ms | 低延迟(JDK 17+) | -XX:+UseZGC |
| SerialGC | 长 | 小堆(< 200MB) | -XX:+UseSerialGC |
| ParallelGC | 中 | 吞吐量优先 | -XX:+UseParallelGC |
graph LR
A["JVM Tuning"] --> B["Heap Size<br/>-Xms = -Xmx"]
A --> C["GC Algorithm<br/>G1 / ZGC"]
A --> D["GC Logging<br/>-Xlog:gc*"]
B --> B1["Container: MaxRAMPercentage"]
C --> C1["Default: G1GC"]
C --> C2["Low latency: ZGC"]
▶ 示例: Docker 环境 JVM 配置
DOCKERFILE
ENTRYPOINT ["java", \
"-XX:+UseG1GC", \
"-XX:MaxRAMPercentage=75.0", \
"-XX:InitialRAMPercentage=50.0", \
"-XX:+UseStringDeduplication", \
"-Xlog:gc*:file=/app/logs/gc.log:time,uptime,level,tags", \
"-jar", "app.jar"]
输出:
TEXT
📖 仅展示
// 执行成功
| 参数 | 含义 | 推荐值 |
|---|---|---|
MaxRAMPercentage |
最大堆占容器内存比例 | 75.0 |
InitialRAMPercentage |
初始堆占容器内存比例 | 50.0 |
+UseG1GC |
使用 G1 垃圾收集器 | 默认开启 |
+UseStringDeduplication |
字符串去重节省内存 | 推荐 |
MaxGCPauseMillis |
GC 目标暂停时间 | 200(G1)/ 无(ZGC) |
▶ 示例: ZGC 低延迟配置
BASH
java -XX:+UseZGC \
-XX:MaxRAMPercentage=75.0 \
-XX:+ZGenerational \
-Xlog:gc*:file=gc.log \
-jar app.jar
输出:
TEXT
📖 仅展示
# 命令执行成功
📌 重点: ZGC 在 JDK 17 中是实验性功能(JDK 21 正式可用)。暂停时间 < 1ms,适合对延迟极度敏感的场景。
4. HikariCP 连接池优化
(1) 连接池参数
| 参数 | 默认值 | 含义 | 推荐值 |
|---|---|---|---|
maximumPoolSize |
10 | 最大连接数 | CPU 核数 × 2 + 磁盘数 |
minimumIdle |
= maxPool | 最小空闲连接 | = maxPoolSize |
connectionTimeout |
30000ms | 获取连接超时 | 3000ms |
idleTimeout |
600000ms | 空闲连接超时 | 600000ms |
maxLifetime |
1800000ms | 连接最大存活时间 | 1800000ms |
leakDetectionThreshold |
0(禁用) | 连接泄漏检测 | 60000ms |
▶ 示例: HikariCP 生产配置
YAML
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 20
connection-timeout: 3000
idle-timeout: 600000
max-lifetime: 1800000
leak-detection-threshold: 60000
pool-name: OrderFlowHikariCP
输出:
TEXT
📖 仅展示
配置文件已生效
▶ 示例: 连接泄漏检测
YAML
# Enable leak detection (log warning if connection held > 60s)
spring:
datasource:
hikari:
leak-detection-threshold: 60000
💻 输出(泄漏时):
TEXT
📖 仅展示
WARN - Connection leak detection triggered for {conn-12345}
Stack trace of the call site that acquired the connection:
at com.orderflow.service.OrderService.getOrder(OrderService.java:45)
...
5. JPA 查询优化
(1) N+1 问题
▶ 示例: N+1 问题演示
JAVA
// BAD: N+1 problem
// 1 SQL to fetch 100 orders + 100 SQLs to fetch items for each order = 101 SQLs!
List<Order> orders = orderRepository.findAll();
for (Order order : orders) {
order.getItems().size(); // Triggers lazy load for each order
}
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例: JOIN FETCH 解决 N+1
JAVA
// GOOD: Single query with JOIN FETCH
@Query("SELECT DISTINCT o FROM Order o JOIN FETCH o.items i JOIN FETCH i.product WHERE o.status = :status")
List<Order> findByStatusWithItems(@Param("status") String status);
输出:
TEXT
📖 仅展示
// 执行成功
▶ 示例: @EntityGraph 声明式加载
JAVA
@Entity
@NamedEntityGraph(
name = "Order.withItemsAndProduct",
attributeNodes = {
@NamedAttributeNode("items"),
@NamedAttributeNode(value = "items", subgraph = "item-product")
},
subgraphs = {
@NamedSubgraph(name = "item-product", attributeNodes = @NamedAttributeNode("product"))
}
)
public class Order { /* ... */ }
// Usage in Repository
@EntityGraph(value = "Order.withItemsAndProduct", type = EntityGraphType.LOAD)
List<Order> findByStatus(String status);
输出:
TEXT
📖 仅展示
// 执行成功
| 方案 | SQL 数量 | 适用场景 | 可维护性 |
|---|---|---|---|
| 默认懒加载 | N+1 | 单个对象查询 | 高 |
| JOIN FETCH | 1 | 特定查询 | 中 |
| @EntityGraph | 1 | 可复用的加载计划 | 高 |
| @BatchSize | N/batchSize | 批量加载 | 高 |
6. 数据库索引优化
(1) 索引策略
▶ 示例: 添加数据库索引
SQL
-- Index for order status queries
CREATE INDEX idx_order_status ON orders(status);
-- Composite index for common query pattern
CREATE INDEX idx_order_status_created ON orders(status, created_at DESC);
-- Index for product search
CREATE INDEX idx_product_name ON products(name);
-- Index for order item product lookup
CREATE INDEX idx_order_item_product ON order_items(product_id);
输出:
TEXT
📖 仅展示
CREATE TABLE
| 索引类型 | 适用场景 | 示例 |
|---|---|---|
| 单列索引 | 单条件查询 | WHERE status = 'PENDING' |
| 复合索引 | 多条件查询 | WHERE status = ? AND created_at > ? |
| 唯一索引 | 唯一约束 | UNIQUE(sku) |
| 覆盖索引 | 索引包含所有查询列 | SELECT status FROM orders WHERE id = ? |
▶ 示例: 慢查询分析
SQL
-- MySQL slow query log configuration
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- Log queries > 1 second
SET GLOBAL log_queries_not_using_indexes = ON;
-- Analyze query execution plan
EXPLAIN SELECT o.*, i.*
FROM orders o
JOIN order_items i ON o.id = i.order_id
WHERE o.status = 'PENDING'
AND o.created_at > '2024-01-01'
ORDER BY o.created_at DESC
LIMIT 20;
输出:
TEXT
📖 仅展示
CREATE TABLE
| EXPLAIN 字段 | 含义 | 关注点 |
|---|---|---|
type |
访问类型 | ALL(全表扫描)需优化 |
key |
使用的索引 | NULL 表示未使用索引 |
rows |
扫描行数 | 越小越好 |
Extra |
额外信息 | Using filesort 需优化 |
7. 综合示例:OrderFlow P99 从 500ms 到 50ms
YAML
# application-prod.yml (optimized)
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 20
connection-timeout: 3000
leak-detection-threshold: 60000
jpa:
hibernate:
ddl-auto: validate
show-sql: false
properties:
hibernate:
default_batch_fetch_size: 100
jdbc:
batch_size: 50
order_inserts: true
order_updates: true
BASH
# JVM parameters (Docker ENTRYPOINT)
java -XX:+UseG1GC \
-XX:MaxRAMPercentage=75.0 \
-XX:InitialRAMPercentage=50.0 \
-XX:+UseStringDeduplication \
-XX:MaxGCPauseMillis=100 \
-Xlog:gc*:file=/app/logs/gc.log:time,uptime,level,tags \
-jar app.jar
JAVA
// Optimized OrderRepository with EntityGraph and batch fetching
public interface OrderRepository extends JpaRepository<Order, Long> {
@EntityGraph(value = "Order.withItemsAndProduct", type = EntityGraphType.LOAD)
@Query("SELECT o FROM Order o WHERE o.status = :status ORDER BY o.createdAt DESC")
Page<Order> findByStatusPaged(@Param("status") String status, Pageable pageable);
@EntityGraph(value = "Order.withItemsAndProduct", type = EntityGraphType.LOAD)
Optional<Order> findById(Long id);
}
// Hibernate batch size configuration eliminates N+1 for collections
// application.yml: hibernate.default_batch_fetch_size=100
| 优化前 | 优化后 | 提升倍数 |
|---|---|---|
| P99: 500ms | P99: 50ms | 10x |
| SQL/请求: 101 | SQL/请求: 1 | 100x |
| GC 暂停: 200ms | GC 暂停: 50ms | 4x |
| 连接等待: 100ms | 连接等待: 2ms | 50x |
❓ 常见问题
Q G1GC 和 ZGC 该选哪个?
A G1GC 是 JDK 17 默认,适合绝大多数场景,暂停时间 10-200ms。ZGC 暂停 < 1ms,适合金融交易等低延迟场景,但吞吐量略低。从 G1GC 开始,有延迟问题再切 ZGC。
Q 连接池大小是不是越大越好?
A 不是。连接池太大导致:1)数据库压力增大;2)上下文切换开销;3)内存占用增加。推荐公式:connections = (CPU cores × 2) + effective_spindle_count。
Q @EntityGraph 和 JOIN FETCH 有什么区别?
A @EntityGraph 是声明式的,定义在 Entity 或 Repository 上可复用;JOIN FETCH 是命令式的,写在每个 @Query 里。复杂场景用 EntityGraph 更好维护。
Q Hibernate batch_size 有什么作用?
A 设置
hibernate.jdbc.batch_size=50 后,INSERT/UPDATE 语句批量执行(50 条一批),减少数据库往返次数。配合 order_inserts=true 效果更好。Q 如何定位性能瓶颈?
A 1)Actuator
/metrics 查看 HTTP 请求耗时;2)GC 日志分析 GC 暂停;3)HikariCP metrics 查看连接池状态;4)MySQL slow query log 查看慢查询;5)APM 工具(SkyWalking/Zipkin)全链路追踪。Q 如何做性能压测?
A 使用 JMeter、Gatling 或 k6。逐步加压(ramp-up),观察响应时间和错误率的拐点。测试场景应覆盖:单接口、混合场景、峰值场景。
📖 小节
- JVM 调优:G1GC 默认够用,ZGC 追求极低延迟,MaxRAMPercentage 控制容器内存
- HikariCP:连接池大小 = CPU 核数 × 2 + 磁盘数,启用 leakDetection 排查泄漏
- N+1 问题:JOIN FETCH 一条 SQL,@EntityGraph 声明式加载,@BatchSize 批量加载
- 数据库索引:为高频查询条件添加索引,EXPLAIN 分析执行计划
- 批量操作:hibernate.jdbc.batch_size + order_inserts 减少数据库往返
- 系统化优化:定位瓶颈 → 量化基线 → 逐层优化 → 验证效果
📝 作业
-
基础题(难度⭐):为 OrderFlow 配置 JVM G1GC 参数和 HikariCP 连接池参数,启用 GC 日志和连接泄漏检测。
-
进阶题(难度⭐⭐):使用
@EntityGraph和JOIN FETCH解决订单列表查询的 N+1 问题,对比优化前后的 SQL 数量和响应时间。 -
挑战题(难度⭐⭐⭐):使用 JMeter 对 OrderFlow 订单列表 API 进行压测,建立性能基线,逐步优化(JVM + 连接池 + N+1 + 索引),记录每步优化的 P50/P99 变化,最终将 P99 优化到 50ms 以下。