性能优化实战——从 JVM 调优到数据库与缓存策略
一、引言
性能优化是后端工程师的核心技能之一。一个系统的性能问题可能出现在任何层面——从应用代码、JVM 运行时、数据库查询到网络传输,任何一个环节都可能成为瓶颈。优秀的性能优化不是一蹴而就的,而是基于数据驱动、层层深入的系统性工作。
本文将从四个关键维度展开:JVM 调优、MySQL 慢查询优化、Redis 缓存策略和接口性能优化,最后介绍系统压测与性能分析的工具链,帮助读者建立全面的性能优化知识体系。
二、JVM 调优
2.1 内存区域与配置
JVM 堆内存是性能调优的核心:
# JVM 参数配置
-Xms4g # 初始堆大小
-Xmx4g # 最大堆大小
-Xmn2g # 年轻代大小
-XX:MetaspaceSize=256m # 元空间初始大小
-XX:MaxMetaspaceSize=256m # 元空间最大大小
-XX:+UseG1GC # 使用 G1 垃圾回收器
-XX:MaxGCPauseMillis=200 # 最大 GC 停顿时间目标
-XX:InitiatingHeapOccupancyPercent=45 # 触发并发标记的堆占用百分比
-XX:+HeapDumpOnOutOfMemoryError # OOM 时生成堆转储
-XX:HeapDumpPath=/var/log/app/heapdump.hprof
2.2 垃圾回收器选择
graph TD
subgraph GC[GC Evolution]
S[Serial<br/>单线程, STW] --> P[Parallel<br/>多线程, 高吞吐]
P --> C[CMS<br/>低延迟, 有碎片]
C --> G1[G1<br/>平衡延迟与吞吐]
G1 --> Z[ZGC<br/>亚毫秒级停顿<br/><10ms]
end
| 垃圾回收器 | 适用场景 | 目标 | JDK 版本 |
|---|---|---|---|
| Serial GC | 单核、小内存(< 100MB) | 简单便携 | 全版本 |
| Parallel GC | 批处理、大内存(8-16GB) | 高吞吐量 | 默认 JDK 8 |
| CMS | 响应优先(已废弃) | 低停顿 | JDK 9+ 不推荐 |
| G1 GC | 通用(4GB+ 推荐) | 可预测停顿 | 默认 JDK 9+ |
| ZGC | 超大堆(几百 GB+) | 亚毫秒停顿 | JDK 11+ 实验性 |
2.3 GC 日志分析
# G1 GC 日志参数
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps
-XX:+PrintAdaptiveSizePolicy
-Xloggc:/var/log/app/gc.log
解读 GC 日志:
2026-05-16T10:30:00.123+0800: [GC pause (G1 Evacuation Pause) (young)
Desired survivor size 30502912 bytes, new threshold 6 (max threshold 15)
- age 1: 1677824 bytes, 1677824 total
- age 2: 524288 bytes, 2202112 total
]
[Parallel Time: 45.2 ms, GC Workers: 8]
[Ext Root Scanning (ms): 1.2]
[Update RS (ms): 3.8]
[Processed Buffers: 32]
[Scan RS (ms): 2.1]
[Code Root Scanning (ms): 0.5]
[Object Copy (ms): 35.6]
[Termination (ms): 1.5]
[GC Worker Other (ms): 0.5]
[Clear CT (ms): 0.3]
[Other: 0.8 ms]
[Eden: 512.0M(512.0M)->0.0B(512.0M) Survivors: 64.0M->64.0M Heap: 1.2G(4.0G)->712.0M(4.0G)]
[Times: user=0.32 sys=0.04, real=0.05 secs]
关键指标:
– GC 停顿时间:real time 是否超过 MaxGCPauseMillis
– Young GC 频率:频繁则加大年轻代
– 晋升大小:age 和 bytes 反映对象晋升情况
– Heap 占用:GC 前后的堆使用率
2.4 线程 Dump 分析
# 获取线程 Dump
jstack -l > threaddump.log
kill -3 # 或发送 SIGQUIT 信号
# 分析死锁
jstack -l | grep -A 20 "deadlock"
常见线程状态:
– RUNNABLE:正在执行
– BLOCKED:等待锁释放
– WAITING / TIMED_WAITING:等待通知或超时
三、MySQL 慢查询优化
3.1 慢查询日志分析
-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 超过 1 秒的记录
SET GLOBAL log_queries_not_using_indexes = ON;
-- 查看慢查询日志位置
SHOW VARIABLES LIKE 'slow_query_log_file';
3.2 EXPLAIN 解析
EXPLAIN SELECT o.*, u.name
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'paid'
AND o.created_at >= '2026-01-01'
ORDER BY o.created_at DESC
LIMIT 20;
EXPLAIN 输出关键字段:
| 字段 | 好的信号 | 坏的信号 |
|---|---|---|
| type | ref, range, const |
ALL(全表扫描) |
| possible_keys | 显示可能用到的索引 | NULL |
| key | 实际使用的索引 | NULL |
| rows | 扫描行数接近返回行数 | 远大于 LIMIT |
| Extra | Using index |
Using filesort, Using temporary |
3.3 索引优化实战
-- 识别未使用索引的查询
SELECT * FROM orders WHERE status = 'paid'; -- 需要创建索引
-- 创建合适的复合索引(ESR 原则)
-- Equality → Sort → Range
ALTER TABLE orders ADD INDEX idx_status_created_user (status, created_at, user_id);
-- 覆盖索引(Extra: Using index)
EXPLAIN SELECT status, created_at, user_id FROM orders WHERE status = 'paid';
3.4 分页优化
-- ❌ 传统分页(深度分页性能差)
SELECT * FROM orders ORDER BY id LIMIT 100000, 20;
-- ✅ 延迟关联
SELECT o.* FROM orders o
INNER JOIN (
SELECT id FROM orders ORDER BY id LIMIT 100000, 20
) AS tmp ON o.id = tmp.id;
-- ✅ 游标分页(推荐)
SELECT * FROM orders
WHERE id > :last_id
ORDER BY id
LIMIT 20;
四、Redis 缓存策略
4.1 缓存模式对比
graph TD
subgraph Strategies[缓存策略]
CP[Cache-Aside
旁路缓存]
RTR[Read-Through
读穿透]
WT[Write-Through
写穿透]
WB[Write-Behind
异步双写]
end
subgraph Tradeoffs[权衡]
CP_T["✅ 实现简单,缓存可控
❌ 缓存击穿风险"]
RTR_T["✅ 应用无感
❌ 库依赖度高"]
WT_T["✅ 数据一致性好
❌ 写入延迟增加"]
WB_T["✅ 写入性能高
❌ 可能丢数据"]
end
4.2 Cache-Aside(旁路缓存)
@Service
public class ProductCacheService {
@Autowired
private RedisTemplate<String, Product> redisTemplate;
private static final String CACHE_PREFIX = "product:";
private static final long TTL = 30 * 60; // 30 分钟
public Product getProduct(Long id) {
String key = CACHE_PREFIX + id;
// 1. 先从缓存获取
Product product = redisTemplate.opsForValue().get(key);
if (product != null) {
return product;
}
// 2. 缓存未命中,从数据库加载
product = productRepository.findById(id).orElse(null);
if (product == null) {
return null;
}
// 3. 写入缓存
redisTemplate.opsForValue().set(key, product, TTL, TimeUnit.SECONDS);
return product;
}
// 4. 更新数据时,先更新数据库再删除缓存
@Transactional
public Product updateProduct(Long id, ProductUpdateRequest request) {
Product product = productRepository.findById(id).orElseThrow();
product.setPrice(request.getPrice());
productRepository.save(product);
// 删除缓存而非更新缓存(懒加载策略)
redisTemplate.delete(CACHE_PREFIX + id);
return product;
}
}
4.3 缓存问题与解决方案
| 问题 | 现象 | 解决方案 |
|---|---|---|
| 缓存穿透 | 请求不存在的数据,穿透 DB | 布隆过滤器 / 缓存空值(短 TTL) |
| 缓存击穿 | 热点 Key 过期,并发请求 | 互斥锁 / 后台续期 |
| 缓存雪崩 | 大量 Key 同时过期 | 随机过期时间 / 多级缓存 |
// 布隆过滤器防穿透
@Component
public class BloomFilterService {
private final BloomFilter<String> bloomFilter;
public BloomFilterService() {
this.bloomFilter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
1000000, // 预计插入数据量
0.01 // 误判率
);
}
public boolean mightContain(String key) {
return bloomFilter.mightContain(key);
}
public void put(String key) {
bloomFilter.put(key);
}
}
// 互斥锁防击穿
public Product getProductWithLock(Long id) {
String cacheKey = "product:" + id;
Product p = redisTemplate.opsForValue().get(cacheKey);
if (p != null) return p;
String lockKey = "lock:product:" + id;
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 5, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
p = productRepository.findById(id).orElse(null);
if (p != null) {
redisTemplate.opsForValue().set(cacheKey, p, TTL, TimeUnit.SECONDS);
}
return p;
} finally {
redisTemplate.delete(lockKey);
}
} else {
Thread.sleep(50);
return getProductWithLock(id); // 自旋重试
}
}
五、接口性能优化
5.1 异步处理
// ✅ 使用 CompletableFuture 并行调用
@Service
public class AggregationService {
@Autowired
private UserServiceClient userClient;
@Autowired
private OrderServiceClient orderClient;
@Autowired
private ProductServiceClient productClient;
@Async("taskExecutor")
public CompletableFuture<UserDTO> getUserAsync(Long id) {
return CompletableFuture.completedFuture(userClient.getUser(id));
}
public DashboardVO getDashboard(Long userId) {
long start = System.currentTimeMillis();
// 串行执行:300ms + 200ms + 150ms = 650ms
// 并行执行:max(300ms, 200ms, 150ms) ≈ 300ms
CompletableFuture<UserDTO> userFuture = CompletableFuture
.supplyAsync(() -> userClient.getUser(userId));
CompletableFuture<List<OrderDTO>> orderFuture = CompletableFuture
.supplyAsync(() -> orderClient.getOrders(userId));
CompletableFuture<List<ProductDTO>> productFuture = CompletableFuture
.supplyAsync(() -> productClient.getRecommendations(userId));
DashboardVO result = CompletableFuture
.allOf(userFuture, orderFuture, productFuture)
.thenApply(v -> DashboardVO.builder()
.user(userFuture.join())
.orders(orderFuture.join())
.recommendations(productFuture.join())
.build())
.join();
log.info("Dashboard built in {} ms", System.currentTimeMillis() - start);
return result;
}
}
5.2 批处理优化
// ❌ N+1 问题
for (Long orderId : orderIds) {
Order order = orderRepository.findById(orderId).get(); // N 次查询
}
// ✅ 批量查询
List<Order> orders = orderRepository.findAllById(orderIds); // 1 次查询
// ✅ 批量写入
int batchSize = 500;
List<Product> products = generateProducts();
for (int i = 0; i < products.size(); i += batchSize) {
int end = Math.min(i + batchSize, products.size());
productRepository.saveAll(products.subList(i, end));
}
5.3 数据库连接池配置
# HikariCP 配置
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
idle-timeout: 300000
connection-timeout: 5000
max-lifetime: 1200000
pool-name: MyAppPool
六、系统压测
6.1 JMeter 压测
100
30
300
api.example.com
443
/api/orders/1
GET
关键指标:
– TPS:每秒事务数
– RT(P50/P90/P99):响应时间分布
– Error Rate:错误率
6.2 async-profiler 火焰图
# 采样 CPU 热点
./profiler.sh -d 30 -e cpu -f flamegraph.html
# 采样分配热点
./profiler.sh -d 30 -e alloc -f alloc.html
# 采样锁竞争
./profiler.sh -d 30 -e lock -f lock.html
火焰图解读:
– X 轴:方法调用,无排序
– Y 轴:调用栈深度
– 宽度:CPU 耗时比例
– 最宽的函数是性能热点
七、总结
性能优化是一个系统工程,需要从全链路视角出发:
- JVM 调优:合理配置堆内存和 GC 策略,G1 是 4-16GB 堆的首选,ZGC 适合超大堆场景
- MySQL 优化:慢查询日志 + EXPLAIN 分析 + 复合索引(ESR 原则)+ 游标分页
- 缓存策略:Cache-Aside 模式是最实用的方案,配合布隆过滤器和互斥锁解决缓存穿透和击穿问题
- 接口优化:异步并行、批量处理、合理的事务粒度
- 压测验证:JMeter 量化性能指标,火焰图定位热点代码
优化的核心原则是”先测量,后优化”。在没有性能数据的情况下盲目优化,往往事倍功半。建立监控系统(Prometheus + Grafana)、链路追踪(Jaeger/SkyWalking)和应用性能管理(APM)体系,让数据驱动你的优化决策,才是可持续的性能优化之道。


暂无评论内容