第 241 章:性能调优 - 缓存与并发
学习目标
- 高级缓存策略
- 异步与并发优化
- 响应式与高并发
- 综合调优方法论
一、缓存进阶
1.1 多级缓存穿透流程
1.2 缓存策略对比
| 策略 | 适用 | 复杂度 |
|---|---|---|
| Read-Aside | 通用 | 中 |
| Write-Through | 读多写少 | 低 |
| Write-Behind | 写多,允许丢失 | 高 |
| Refresh-Ahead | 高频热点 | 中 |
1.3 缓存预热
java
@Component
public class CacheWarmer {
@Autowired
private Cache<String, Product> localCache;
@Autowired
private RedisTemplate<String, Product> redis;
public void warm(StopWatch sw) {
sw.start("加载热门商品");
List<Long> topIds = productRepo.findTopIds(0, 5000);
Map<String, Product> map = new HashMap<>();
for (Long id : topIds) {
Product p = productRepo.findById(id);
if (p != null) {
map.put("product:" + id, p);
localCache.put(id.toString(), p);
}
}
redis.opsForValue().multiSet(map);
sw.stop();
log.info("预热完成,加载 {} 条", topIds.size());
}
}1.4 缓存击穿-分布式锁
java
public Product getHotProduct(Long id) {
Product p = cache.get("p:" + id);
if (p != null) return p;
// 分布式锁
RLock lock = redisson.getLock("lock:p:" + id);
if (lock.tryLock(0, 30, TimeUnit.SECONDS)) {
try {
// 二次检查
p = cache.get("p:" + id);
if (p == null) {
p = dbMapper.findById(id);
if (p != null) {
cache.put("p:" + id, p, 60, TimeUnit.SECONDS);
}
}
} finally {
lock.unlock();
}
} else {
// 等待 50ms 重试
return retry();
}
return p;
}1.5 业务分离缓存
yaml
缓存按业务分类:
- 用户缓存: user:{id}
- 商品缓存: product:{id}
- 配置缓存: config:{key}
- 列表缓存: list:{key}:page:{p}
- 计数缓存: count:{key}二、Redis 性能
2.1 慢命令
bash
# 慢日志
SLOWLOG GET 10
SLOWLOG LEN
SLOWLOG RESET
# 配置
slowlog-log-slower-than 10000 # 10ms
slowlog-max-len 128常见慢命令:
KEYS *(生产禁用)HGETALL(大 hash)ZRANGE(大 zset)SUNION(大 set)SMEMBERS(大 set)
替代:
SCAN(替代 KEYS)- 拆分大 key
- 用 ID 列表缓存
2.2 Pipeline
java
public List<Product> batchQuery(List<Long> ids) {
return redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
for (Long id : ids) {
connection.stringCommands().get(("p:" + id).getBytes());
}
return null;
});
}减少 RTT,适合批量操作。
2.3 Lua 脚本
lua
-- 多 key 原子操作
local p1 = redis.call('GET', KEYS[1])
local p2 = redis.call('GET', KEYS[2])
if p1 and p2 then
return {p1, p2}
end
return nil2.4 客户端优化
yaml
Lettuce 默认: Netty,共享连接
Jedis: 阻塞,需连接池
lettuce:
pool:
enabled: true
max-active: 16
max-idle: 82.5 大 key 处理
bash
# 找大 key
redis-cli --bigkeys
# OBJECT IDLETIME
OBJECT IDLETIME bigkey拆分方案:
- 大 JSON → HASH 字段
- 大 set → 多 set + 集合运算
- 大 zset → 滑动窗口
2.6 Redis 内存淘汰
yaml
maxmemory 4gb
maxmemory-policy allkeys-lfu # 优先淘汰最不频繁
# 备选: allkeys-lru / volatile-lru2.7 Redis Cluster
三、并发优化
3.1 ForkJoin 并行处理
java
public class ParallelProcessor extends RecursiveTask<Long> {
private final List<Long> numbers;
public ParallelProcessor(List<Long> numbers) {
this.numbers = numbers;
}
@Override
protected Long compute() {
if (numbers.size() < 1000) {
return numbers.stream().mapToLong(Long::longValue).sum();
}
// 拆分
int mid = numbers.size() / 2;
ParallelProcessor left = new ParallelProcessor(numbers.subList(0, mid));
ParallelProcessor right = new ParallelProcessor(numbers.subList(mid, numbers.size()));
left.fork();
return right.compute() + left.join();
}
}
// 使用 ForkJoinPool
ForkJoinPool pool = new ForkJoinPool();
Long result = pool.invoke(new ParallelProcessor(bigList));3.2 CompletableFuture
java
public CompletableFuture<UserDTO> getUserAsync(Long id) {
return CompletableFuture.supplyAsync(() -> userRepo.findById(id))
.thenApply(this::toDTO)
.thenCompose(dto -> enrichWithOrders(dto));
}
public UserDTO enrichWithOrders(UserDTO dto) {
return CompletableFuture.supplyAsync(() -> {
dto.setOrders(orderService.findByUser(dto.getId()));
return dto;
}).join();
}并行多个独立操作:
java
public Map<String, Object> dashboard(Long userId) {
CompletableFuture<UserDTO> user = CompletableFuture
.supplyAsync(() -> userService.findById(userId));
CompletableFuture<List<Order>> orders = CompletableFuture
.supplyAsync(() -> orderService.findByUser(userId));
CompletableFuture<List<Coupon>> coupons = CompletableFuture
.supplyAsync(() -> couponService.findByUser(userId));
CompletableFuture.allOf(user, orders, coupons).join();
return Map.of(
"user", user.join(),
"orders", orders.join(),
"coupons", coupons.join()
);
}3.3 异步 I/O
Servlet 3.0+ 异步
java
@GetMapping("/async")
public CompletableFuture<String> async() {
return CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(100); } catch (Exception e) {}
return "done";
});
}WebFlux 响应式
java
@GetMapping("/users/{id}")
public Mono<UserDTO> getUser(@PathVariable Long id) {
return userService.findById(id)
.map(this::toDTO);
}
@GetMapping("/users")
public Flux<UserDTO> list() {
return userService.findAll()
.map(this::toDTO);
}优势:
- 少量线程处理大量并发
- 适合 IO 密集型
劣势:
- 学习曲线陡
- 异步传染
四、消息队列优化
4.1 批量消费
java
@KafkaListener(topics = "orders", batch = "true")
public void batchConsume(List<OrderMessage> messages) {
for (OrderMessage msg : messages) {
orderService.process(msg);
}
// 一次 ack,少网络
}4.2 异步 Commit
yaml
spring:
kafka:
consumer:
ack-mode: manual_immediatejava
@KafkaListener(...)
public void onMessage(OrderMessage msg,
@Header(KafkaHeaders.OFFSET) List<Long> offsets,
Acknowledgment ack) {
try {
orderService.process(msg);
ack.acknowledge();
} catch (Exception e) {
// 不 ack,下次重试
}
}4.3 消费限速
yaml
spring:
kafka:
listener:
concurrency: 3
max-poll-records: 500java
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setBatchListener(true);
factory.setConcurrency(3);
factory.getContainerProperties().setMaxPollRecords(500);
return factory;
}五、Tomcat 调优
yaml
server:
tomcat:
threads:
max: 200 # 最大工作线程
min-spare: 50
accept-count: 100 # accept queue
max-connections: 10000 # 最大连接
connection-timeout: 20s
keep-alive-timeout: 60s
mbeanregistry:
enabled: true
# APR Tomcat(高并发)
# 切换 native 模式公式:
线程数 = CPU 核数 * 2 + 1 (计算型)
= (IO 平均耗时 / CPU 耗时 + 1) * CPU 核数 (IO 型)六、NIO 与 Zero Copy
java
// ❌ 4 次拷贝(2 次 DMA + 2 次 CPU)
FileInputStream.read()
JVM buffer
Socket.send()
// ✅ Zero Copy
FileChannel channel = new FileInputStream(file).getChannel();
channel.transferTo(0, channel.size(), socketChannel);
// 2 次 DMA 拷贝工具:
- Netty
- OkHttp
- Kafka 内部
七、压缩与序列化
7.1 JSON 序列化
java
// Jackson 调优
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.registerModule(new JavaTimeModule());
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// 字段不序列化
@JsonIgnore
private String internalField;7.2 字节码大小
yaml
选择:
- Jackson: 通用
- Gson: 简单
- Protobuf: 高性能 + IDL
- Kryo: 高性能二进制
- Hibernate Validator: 校验7.3 压缩
yaml
spring:
gzip:
enabled: true
min-request-size: 1024
mime-types: application/json,text/html
server:
compression:
enabled: true
mime-types: application/json,application/xml
min-response-size: 1024八、网络优化
8.1 Keep-Alive
yaml
server:
tomcat:
keep-alive-timeout: 60s
max-keep-alive-requests: 1008.2 Netty 调优
java
ServerBootstrap b = new ServerBootstrap();
b.childOption(ChannelOption.SO_RCVBUF, 32 * 1024)
.childOption(ChannelOption.SO_SNDBUF, 32 * 1024)
.childOption(ChannelOption.TCP_NODELAY, true) // 关闭 Nagle
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.childOption(ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, 64 * 1024);8.3 长连接 vs 短连接
yaml
内网: 长连接(keep-alive)
外部: HTTP/2 多路复用
RPC: gRPC(HTTP/2 + Protobuf)九、响应式编程(Spring WebFlux)
9.1 适用场景
yaml
适合:
- 大量 IO
- 长连接
- 流式响应
- 实时推送
不适合:
- CPU 密集
- 大量阻塞库(JDBC)
- 小并发9.2 实际应用
java
@RestController
public class ReactiveController {
@GetMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> sse() {
return Flux.interval(Duration.ofSeconds(1))
.map(i -> "data: " + Instant.now())
.take(60);
}
@GetMapping("/products")
public Flux<ProductDTO> allProducts() {
return reactiveRepo.findAll();
}
}十、配置层面优化
10.1 连接复用
java
// HikariCP 默认池化连接
// Netty Channel 复用
// Redis Lettuce 连接复用
// HTTP/2 多路复用10.2 超时设置
yaml
feign:
client:
config:
default:
connectTimeout: 3000 # 3 秒
readTimeout: 8000
# 总是设置超时,默认 60 秒太长10.3 限流保护
yaml
resilience4j:
bulkhead:
instances:
inventory:
maxConcurrentCalls: 100
maxWaitDuration: 500ms
feign:
sentinel:
enabled: true十一、典型调优案例
11.1 P99 突然飙到 5 秒
排查步骤:
yaml
1. 检查最近的发布: git diff HEAD~5
2. 看 GC 日志:最近是不是 GC 频繁
3. 看慢查询: db 是不是挂了
4. 看 Network IO:网卡是不是跑满
5. 看磁盘:磁盘 IO 是瓶颈?
6. 用 Arthas:trace 慢方法栈11.2 QPS 没涨但 P99 涨
yaml
可能原因:
- 数据量变大(分页越查越慢)
- 内存泄漏(stw 时间增长)
- 索引失效(新加数据导致)
- 缓存命中率下降11.3 CPU 飙到 100%
bash
# 1. 找 hot thread
top -Hp <pid>
# 2. jstack 拿到线程栈
printf '%x\n' <tid>
jstack <pid> | grep <hex_tid>
# 3. Arthas dashboard
thread -n 3十二、性能方法论
12.1 二八原则
yaml
80% 性能问题集中在 20% 代码
→ 火焰图找热点
→ 优化这 20%12.2 调优顺序
yaml
1. 排查瓶颈(基准测)
2. SQL 和索引
3. 缓存策略
4. 并发模型
5. GC/JVM
6. 架构改造12.3 A/B 测试
yaml
所有优化都要:
- 可量化(具体指标改善多少)
- 可对比(A/B 测试)
- 可回退(灰度发布)十三、本章小结
| 主题 | 关键 |
|---|---|
| 缓存 | 多级 + 预热 + 防三大问题 |
| Redis | 命令 + Pipeline + Cluster |
| 并发 | 线程池 + 异步 + 无锁 |
| 响应式 | WebFlux + Netty |
| 配置 | 超时 + 限流 + Keep-Alive |
动手练习
- 用 CompletableFuture 并行查询 3 个独立服务
- 配置 Redis Cluster,主从切换 < 1 秒
- 用火焰图找出 CPU 热点并优化
- 设置完整的超时与限流,观察压测变化
下一章:第 242 章:综合调优实战