高并发系统的性能瓶颈从来都不是单点问题,而是端到端全链路的木桶效应——哪怕你把数据库优化到极致,只要接入层、网关层、服务层的任何一个环节出现短板,整个系统的吞吐量和延迟都会被严重拖累。
一、高并发性能优化的核心认知与指标体系
想要做好性能优化,首先要建立正确的认知体系,避免陷入常见的优化误区。
1.1 端到端优化的核心定义
端到端优化,是指覆盖用户发起请求到收到响应的完整链路,对每一个环节进行针对性优化,而非只聚焦于数据库等单点环节。实际生产环境中,80%的性能损耗往往发生在非数据库环节,只优化数据库很难获得质的提升。
1.2 核心性能指标
高并发系统的性能评估,核心关注三个指标:
- 吞吐量(QPS/TPS):系统每秒能处理的请求数,是高并发能力的核心衡量标准
- 延迟(RT):请求从发起到收到响应的时间,重点关注P95、P99延迟(95%/99%的请求能在该时间内完成),它决定了用户的最差体验,比平均延迟更重要
- 可用性:系统正常服务的时间占比,高并发场景下性能瓶颈往往会引发系统雪崩,直接降低可用性
1.3 常见优化误区
- 误区1:过早优化:在没有压测和瓶颈定位的情况下盲目优化,反而增加系统复杂度,甚至引入bug
- 误区2:单点优化:只优化数据库,忽略接入层、网关层、网络层的优化,无法突破全链路的性能天花板
- 误区3:只关注平均延迟:很多系统平均延迟很低,但P99延迟极高,用户实际体验极差
- 误区4:过度优化:为了提升1%的性能,大幅增加系统复杂度,导致维护成本飙升
- 误区5:忽略底层系统优化:很多时候系统瓶颈是操作系统参数设置不当,而非应用代码问题
二、端到端全链路优化架构体系
高并发系统的完整请求链路分为7个核心环节,每个环节都有对应的优化空间,整体架构如下:
本文将按照链路顺序,逐个拆解每个环节的优化逻辑、落地方法和可复用代码。
三、用户端与接入层性能优化
这个环节是请求的第一跳,也是最容易被忽略的环节,用户感知到的卡顿,80%都发生在这个环节,而非服务端。核心优化方向是:减少请求数、降低网络延迟、提升接入层吞吐量。
3.1 网络协议优化:从HTTP/1.1到HTTP/3
- HTTP/1.1的核心问题:队头阻塞,同一个TCP连接同一时间只能处理一个请求,前一个请求未完成时,后续请求只能排队
- HTTP/2的优化:多路复用,同一个TCP连接可同时处理多个请求,解决了应用层的队头阻塞,但TCP本身的队头阻塞依然存在——丢包后整个连接的所有请求都要等待重传
- HTTP/3的核心优势:基于QUIC协议(UDP传输),彻底解决TCP的队头阻塞,每个请求都是独立的流,丢包只会影响单个请求;支持0-RTT握手,比TLS1.3的1-RTT更快;支持连接迁移,用户网络切换时连接不会断开。
最新稳定版Nginx 1.26.2已正式支持HTTP/3,下面是配置:
user nginx;
worker_processes auto;
worker_rlimit_nofile 1048576;
events {
use epoll;
worker_connections 1048576;
multi_accept on;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
keepalive_requests 10000;
types_hash_max_size 2048;
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
server {
listen 443 quic reuseport;
listen 443 ssl;
http2 on;
http3 on;
server_name example.com;
ssl_certificate /etc/nginx/cert/fullchain.pem;
ssl_certificate_key /etc/nginx/cert/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
add_header Alt-Svc 'h3=":443"; ma=86400' always;
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
root /usr/share/nginx/html;
index index.html;
location /static/ {
expires 365d;
add_header Cache-Control "public, immutable";
}
location /api/ {
proxy_pass http://gateway-cluster;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 3s;
proxy_send_timeout 10s;
proxy_read_timeout 10s;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
upstream gateway-cluster {
server 192.168.1.10:8080;
server 192.168.1.11:8080;
keepalive 32;
}
}
配置核心优化点:
worker_processes auto:自动匹配CPU核心数,充分利用多核性能worker_rlimit_nofile 1048576:调整单进程最大文件打开数,解决高并发下文件描述符不足的问题- 开启HTTP/3、HTTP/2,兼容HTTP/1.1,同时开启TLS1.3,降低握手延迟
- 开启gzip和brotli双压缩,静态资源体积减少60%以上
- 静态资源设置长缓存+immutable,避免重复请求
- upstream开启keepalive长连接,减少和网关层的TCP握手开销
3.2 CDN优化策略
CDN的核心作用是将静态资源缓存到离用户最近的节点,减少跨地域网络延迟,核心优化点:
- 全量静态资源接入:HTML、JS、CSS、图片、视频、字体等所有非动态接口内容全部接入CDN
- 缓存策略优化:静态资源通过文件hash实现版本更新,设置365天长缓存;热点动态接口可通过CDN边缘计算能力缓存,减少回源请求
- 预热与预加载:大促前提前预热热点资源到全量CDN节点;对首屏资源开启DNS预解析、TCP预连接,降低首屏加载时间
- 协议对齐:CDN节点开启HTTP/3和TLS1.3,和接入层保持一致
3.3 四层负载均衡优化
四层负载均衡(LVS)工作在TCP层,负责将流量转发到Nginx集群,核心优化点:
- 采用DR直接路由模式,回程流量不经过LVS,吞吐量提升10倍以上
- 调整Linux内核参数,开启
tcp_tw_reuse、tcp_syncookies,应对高并发TCP连接 - 禁用不必要的会话保持,避免用户请求固定到单个Nginx节点,导致负载不均
四、API网关层性能优化
API网关是后端服务的流量入口,网关的性能直接决定了整个系统的吞吐量上限。本文采用最新稳定版Spring Cloud Gateway 4.2.3,基于Spring Boot 3.3.3和JDK21 LTS实现,底层基于Netty 4.1.112.Final异步非阻塞架构,性能是传统Zuul网关的10倍以上。
4.1 网关核心优化方向
网关的性能瓶颈主要集中在路由匹配、过滤器链执行、网络IO、限流熔断逻辑,核心优化如下:
4.1.1 路由规则优化
- 高频访问的路由按优先级放在最前面,减少匹配次数
- 避免使用复杂正则作为路由断言,尽量用Path、Method等简单断言
- 禁用所有不需要的路由,减少匹配循环次数
4.1.2 过滤器链优化
- 只保留业务必需的过滤器,禁用所有非必要的内置过滤器
- 过滤器逻辑尽量轻量化,避免在过滤器中执行耗时IO操作,必须执行时需用异步非阻塞方式
- 合并相同逻辑的过滤器,减少过滤器链长度,降低方法调用开销
4.1.3 Netty底层参数调优
Spring Cloud Gateway底层基于Netty,参数配置直接决定网关性能,配置如下(application.yml):
spring:
cloud:
gateway:
httpclient:
connect-timeout: 3000
response-timeout: 10s
pool:
type: elastic
max-idle-time: 30s
max-life-time: 60s
max-connections: 10000
acquire-timeout: 3000
server:
netty:
connection-timeout: 3s
idle-timeout: 30s
max-initial-line-length: 4096
max-header-size: 8192
max-chunk-size: 8192
server:
netty:
connection-timeout: 3s
reactor:
netty:
worker-count: 16
selector-count: 4
配置说明:
httpclient.pool:配置转发请求的弹性连接池,最大连接数10000,应对高并发转发reactor.netty.worker-count:Netty worker线程数,IO密集型场景设置为CPU核心数*4,8核CPU设置为16- 所有超时时间设置合理值,避免慢请求占用连接导致连接池耗尽
4.1.4 限流熔断优化
高并发场景下,限流熔断是网关的核心能力,避免后端服务被流量打垮。我们采用Redis令牌桶算法实现分布式限流,基于Spring Cloud Gateway内置的RequestRateLimiter,实现如下:
首先引入依赖(pom.xml):
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.3</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>gateway-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gateway-demo</name>
<properties>
<java.version>21</java.version>
<spring-cloud.version>2023.0.3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
然后实现限流维度的KeyResolver,指定按用户ID/IP限流:
package com.example.gatewaydemo.config;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
import java.util.Optional;
@Configuration
public class GatewayRateLimitConfig {
@Bean
public KeyResolver userKeyResolver() {
return exchange -> Mono.just(
Optional.ofNullable(exchange.getRequest().getHeaders().getFirst("X-User-Id"))
.orElseGet(() -> exchange.getRequest().getRemoteAddress().getAddress().getHostAddress())
);
}
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}
最后是完整的路由、限流、熔断配置(application.yml):
spring:
application:
name: gateway-demo
data:
redis:
host: 192.168.1.20
port: 6379
password: your-redis-password
lettuce:
pool:
max-active: 100
max-idle: 50
min-idle: 10
max-wait: 3000ms
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1000
redis-rate-limiter.burstCapacity: 2000
redis-rate-limiter.requestedTokens: 1
key-resolver: "#{@userKeyResolver}"
- name: CircuitBreaker
args:
name: userServiceCircuitBreaker
fallbackUri: forward:/fallback/user
- id: order-service
uri: lb://order-service
predicates:
- Path=/order/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 500
redis-rate-limiter.burstCapacity: 1000
key-resolver: "#{@ipKeyResolver}"
- name: CircuitBreaker
args:
name: orderServiceCircuitBreaker
fallbackUri: forward:/fallback/order
httpclient:
connect-timeout: 3000
response-timeout: 10s
pool:
type: elastic
max-connections: 10000
max-idle-time: 30s
acquire-timeout: 3000
resilience4j:
circuitbreaker:
instances:
userServiceCircuitBreaker:
slidingWindowSize: 100
failureRateThreshold: 50
waitDurationInOpenState: 10000
permittedNumberOfCallsInHalfOpenState: 10
orderServiceCircuitBreaker:
slidingWindowSize: 100
failureRateThreshold: 50
waitDurationInOpenState: 10000
permittedNumberOfCallsInHalfOpenState: 10
server:
port: 8080
management:
endpoints:
web:
exposure:
include: health,info,prometheus,gateway
endpoint:
health:
show-details: always
配置说明:
replenishRate:令牌桶每秒填充的令牌数,即每秒允许的平均请求数burstCapacity:令牌桶最大容量,即允许的突发请求数- 熔断配置:50%请求失败时熔断器打开,10秒后进入半开状态,允许10个请求测试服务可用性
- 集成Prometheus监控,可实时观察网关的QPS、延迟、限流次数、熔断次数等指标
经过以上优化,Spring Cloud Gateway单节点(8核16G)可稳定支撑5万+QPS,P99延迟控制在10ms以内。
五、服务层核心性能优化
服务层是业务逻辑的核心,本文基于JDK21 LTS、Spring Boot 3.3.3,从虚拟线程、线程池、Web容器、接口优化、JVM调优五个维度,拆解全量优化方案。
5.1 JDK21虚拟线程:高并发的终极利器
JDK21正式发布的虚拟线程,是Java高并发编程的革命性特性,彻底解决了传统平台线程在IO密集型场景下的性能瓶颈。
5.1.1 虚拟线程的底层原理
传统平台线程与操作系统内核线程一一对应,每个平台线程默认栈内存为1M,创建1万个平台线程就需要10G内存,且内核线程调度开销极大,上下文切换需要内核态与用户态的切换,因此传统Java应用最多只能创建几千个平台线程,无法应对百万级并发。
虚拟线程是JVM管理的轻量级用户态线程,与内核线程是M:N的映射关系,多个虚拟线程可挂载在同一个平台线程(载体线程)上执行,每个虚拟线程的栈内存仅几百字节,JVM可轻松创建上百万个虚拟线程。
最核心的优化是:当虚拟线程执行阻塞IO操作(网络调用、数据库查询、Redis调用等)时,JVM会将虚拟线程从载体线程上卸载,让载体线程执行其他虚拟线程;阻塞操作完成后,再将虚拟线程重新挂载到载体线程执行。这样载体线程始终处于运行状态,CPU利用率可提升至90%以上,彻底解决了IO密集型场景下的线程阻塞问题。
这里明确区分虚拟线程与平台线程的核心差异:
| 特性 | 虚拟线程 | 平台线程 |
| 调度主体 | JVM用户态调度 | 操作系统内核调度 |
| 内存占用 | 几百字节 | 1M左右 |
| 最大数量 | 百万级 | 几千级 |
| 上下文切换开销 | 极低,用户态切换 | 极高,内核态切换 |
| 适用场景 | IO密集型任务 | CPU密集型任务 |
5.1.2 虚拟线程的正确使用
Spring Boot 3.2+已完美支持虚拟线程,仅需一行配置即可开启,所有Spring MVC请求、@Async异步方法、定时任务都会自动使用虚拟线程执行,无需修改业务代码。
application.properties配置:
spring.threads.virtual.enabled=true
虚拟线程的完整使用示例,包含异步编排:
package com.example.service;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
public class UserService {
private final UserMapper userMapper;
private final OrderClient orderClient;
private final RedisClient redisClient;
public UserService(UserMapper userMapper, OrderClient orderClient, RedisClient redisClient) {
this.userMapper = userMapper;
this.orderClient = orderClient;
this.redisClient = redisClient;
}
// 同步接口,开启虚拟线程后自动用虚拟线程执行
public UserDetailVO getUserDetail(Long userId) {
// 阻塞IO1:查询Redis
UserCacheVO cacheVO = redisClient.get("user:" + userId, UserCacheVO.class);
if (cacheVO != null) {
return convertToDetailVO(cacheVO);
}
// 阻塞IO2:查询数据库
UserDO userDO = userMapper.selectById(userId);
if (userDO == null) {
throw new RuntimeException("用户不存在");
}
// 阻塞IO3:远程调用订单服务
List<OrderVO> orderList = orderClient.getOrderListByUserId(userId);
UserDetailVO detailVO = convertToDetailVO(userDO, orderList);
// 异步写入Redis,不阻塞主线程
saveUserCacheAsync(userId, detailVO);
return detailVO;
}
// 异步方法,自动用虚拟线程执行
@Async
public CompletableFuture<Void> saveUserCacheAsync(Long userId, UserDetailVO detailVO) {
redisClient.setEx("user:" + userId, 300, detailVO);
return CompletableFuture.completedFuture(null);
}
// 异步编排,并行执行多个阻塞IO,大幅降低接口延迟
public UserDetailVO getUserDetailAsync(Long userId) {
// 并行查询用户信息
CompletableFuture<UserDO> userFuture = CompletableFuture.supplyAsync(
() -> userMapper.selectById(userId)
);
// 并行查询订单列表
CompletableFuture<List<OrderVO>> orderFuture = CompletableFuture.supplyAsync(
() -> orderClient.getOrderListByUserId(userId)
);
// 等待所有异步任务完成,合并结果
return CompletableFuture.allOf(userFuture, orderFuture)
.thenApply(v -> {
UserDO userDO = userFuture.join();
List<OrderVO> orderList = orderFuture.join();
return convertToDetailVO(userDO, orderList);
}).join();
}
}
5.1.3 虚拟线程的避坑指南
- 不要用虚拟线程执行CPU密集型任务:虚拟线程的优势是处理阻塞IO,CPU密集型任务会持续占用载体线程,导致其他虚拟线程无法调度,性能反而不如平台线程。CPU密集型任务应使用平台线程池,核心线程数设置为CPU核心数+1
- 避免使用synchronized同步块:JDK21已对synchronized做了优化,但虚拟线程阻塞时不会释放载体线程,会导致载体线程被阻塞。建议使用
ReentrantLock,它支持虚拟线程的友好阻塞,会在虚拟线程阻塞时释放载体线程 - 不要用ThreadLocal存储大量数据:创建上百万个虚拟线程时,ThreadLocal会导致内存占用飙升,建议使用JDK21引入的
ScopedValue,专为虚拟线程设计,内存占用更低、性能更好 - 不要池化虚拟线程:虚拟线程的创建成本极低,每次任务创建新的虚拟线程即可,池化反而会限制虚拟线程的数量,失去其核心优势,JDK官方明确不建议池化虚拟线程
5.2 线程池的正确使用与优化
除了虚拟线程,传统平台线程池在CPU密集型场景下依然必不可少,生产环境必须避免线程池的错误用法。
5.2.1 线程池参数的正确设置
线程池参数的核心设置原则是:根据任务类型(CPU密集型/IO密集型)匹配合理参数。
- CPU密集型任务:任务以计算、逻辑处理为主,CPU利用率高,阻塞时间少
- 核心线程数:CPU核心数 + 1(避免CPU核心因页缺失暂停,保证CPU利用率)
- 最大线程数:与核心线程数一致,避免过多线程导致CPU上下文切换频繁
- 队列:有界队列,容量设置为1000左右,避免任务堆积导致OOM
- 拒绝策略:CallerRunsPolicy,让调用线程执行任务,起到流量削峰的作用
- IO密集型任务:任务以阻塞IO操作为主,CPU利用率低,大部分时间处于阻塞等待状态
- 核心线程数:CPU核心数 * 2
- 最大线程数:CPU核心数 * 10,阻塞时间越长,最大线程数可适当增大
- 队列:有界队列,容量设置为最大线程数的10倍左右
- 拒绝策略:AbortPolicy,直接抛出异常,避免系统过载
这里纠正一个常见错误:Executors.newFixedThreadPool()创建的线程池使用无界队列,高并发场景下会导致任务无限堆积,最终引发OOM,生产环境必须用ThreadPoolExecutor手动创建线程池,设置有界队列。
5.2.2 线程池配置示例
package com.example.config;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.*;
@Configuration
public class ThreadPoolConfig {
private static final int CPU_CORE = Runtime.getRuntime().availableProcessors();
// CPU密集型任务线程池
@Bean("cpuIntensiveThreadPool")
public ExecutorService cpuIntensiveThreadPool() {
return new ThreadPoolExecutor(
CPU_CORE + 1,
CPU_CORE + 1,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(1000),
new ThreadFactoryBuilder().setNameFormat("cpu-pool-%d").setDaemon(true).build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
// IO密集型任务平台线程池(不使用虚拟线程时使用)
@Bean("ioIntensiveThreadPool")
public ExecutorService ioIntensiveThreadPool() {
return new ThreadPoolExecutor(
CPU_CORE * 2,
CPU_CORE * 10,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10000),
new ThreadFactoryBuilder().setNameFormat("io-pool-%d").setDaemon(true).build(),
new ThreadPoolExecutor.AbortPolicy()
);
}
}
5.3 Web容器优化
Spring Boot默认的Web容器是Tomcat,高并发场景下,Undertow的性能更优,它基于XNIO的异步非阻塞架构,吞吐量更高、资源占用更低。
切换Undertow只需修改pom.xml,排除Tomcat并引入Undertow:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
Undertow生产级配置(application.properties):
server.undertow.threads.io=16
server.undertow.threads.worker=200
server.undertow.max-http-post-size=10MB
server.undertow.http2.enabled=true
server.undertow.no-request-timeout=60000
server.undertow.options.socket.TCP_NODELAY=true
server.undertow.options.socket.SO_REUSEADDR=true
server.undertow.options.socket.SO_KEEPALIVE=true
配置说明:
io线程数:负责处理IO事件,默认是CPU核心数*2,8核CPU设置为16worker线程数:负责处理业务逻辑,开启虚拟线程后可适当调小,因为业务逻辑由虚拟线程执行
5.4 接口性能优化
接口性能优化的核心原则是:减少阻塞、减少IO、减少计算、并行执行。
- 异步化:将非核心同步操作改为异步操作,比如日志记录、缓存写入、消息发送,不阻塞主线程,降低接口RT
- 并行化:多个无依赖的IO操作,用
CompletableFuture并行执行,将串行RT转为并行RT,比如同时查询用户、订单、商品信息,接口RT可从300ms降至100ms - 序列化优化:JSON序列化性能损耗较大,高并发场景建议使用Protocol Buffers(Protobuf)序列化,序列化速度是JSON的5-10倍,序列化后体积仅为JSON的1/3,大幅降低网络传输和CPU开销
- 避免频繁GC:接口中避免创建大量临时对象,尤其是大对象,比如字符串拼接使用
StringBuilder,而非+号,避免创建大量String对象 - 懒加载:只加载需要的数据,避免全量查询,比如分页查询只返回当前页数据,VO对象只返回前端需要的字段,减少序列化开销
Protobuf序列化实战示例
首先引入依赖:
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.25.5</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>1.66.0</version>
</dependency>
编写Protobuf schema文件(user.proto):
syntax = "proto3";
option java_package = "com.example.proto";
option java_outer_classname = "UserProto";
message UserVO {
int64 user_id = 1;
string user_name = 2;
int32 age = 3;
string phone = 4;
repeated OrderVO order_list = 5;
}
message OrderVO {
int64 order_id = 1;
string order_no = 2;
int32 amount = 3;
int64 create_time = 4;
}
通过Protobuf编译器生成Java类后,即可在代码中使用:
// 序列化
UserProto.UserVO userVO = UserProto.UserVO.newBuilder()
.setUserId(1L)
.setUserName("test")
.setAge(20)
.setPhone("13800138000")
.addOrderList(UserProto.OrderVO.newBuilder()
.setOrderId(1L)
.setOrderNo("ORDER123456")
.setAmount(100)
.setCreateTime(System.currentTimeMillis())
.build())
.build();
byte[] bytes = userVO.toByteArray();
// 反序列化
UserProto.UserVO parsedUser = UserProto.UserVO.parseFrom(bytes);
5.5 JVM优化
JDK21的默认JVM参数已做了大量优化,默认使用G1垃圾收集器,大部分应用无需过度优化,仅需调整几个核心参数即可。
5.5.1 JVM参数配置
-Xms4g -Xmx4g -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/jvm/heapdump.hprof -Xlog:gc*:/var/log/jvm/gc.log -XX:+UseStringDeduplication -XX:+OptimizeStringConcat
参数说明:
-Xms4g -Xmx4g:堆内存初始值与最大值一致,避免堆内存动态调整的开销,生产环境建议设置为物理内存的50%-70%-XX:NewRatio=2:老年代与新生代的比例为2:1,新生代占堆内存的1/3,适配大部分业务场景-XX:+UseG1GC:使用G1垃圾收集器,JDK21默认,适配大内存、低延迟场景-XX:MaxGCPauseMillis=200:设置最大GC停顿时间为200ms,G1会自动调整新生代大小满足该要求-XX:+UseStringDeduplication:开启字符串去重,减少字符串内存占用-XX:+HeapDumpOnOutOfMemoryError:OOM时自动生成堆转储文件,方便排查问题
5.5.2 JVM优化避坑指南
- 不要盲目调大新生代:新生代太大会导致Young GC停顿时间变长,太小会导致频繁Young GC,需根据GC日志调整
- 不要使用CMS收集器:CMS已在JDK14中被移除,JDK21已不支持,建议使用G1或ZGC(ZGC停顿时间可控制在1ms以内,适配对延迟要求极高的场景)
- 不要设置过大的堆内存:堆内存太大会导致Full GC停顿时间变长,32G以上的堆内存会禁用压缩指针,导致内存占用反而变大
- 所有优化必须基于GC日志和压测结果,无数据支撑的优化都是盲目优化
六、缓存层全链路性能优化
缓存是高并发系统的核心组件,80%的高并发场景都是读多写少,缓存可将99%的读请求拦截在数据库之前,大幅降低数据库压力。本文基于最新稳定版Redis 7.4.1,从多级缓存设计、缓存异常场景解决、Redis性能优化三个维度,拆解全量优化方案。
6.1 多级缓存架构设计
高并发场景下,单级Redis缓存无法满足极致性能需求,需设计多级缓存架构,将热点数据尽量放在离CPU最近的地方,减少网络IO开销,整体流程如下:
多级缓存的层级:
- L1级缓存:CPU缓存,JVM内置,开发人员可通过代码优化提升命中率,比如连续数组访问比随机链表访问性能高很多
- L2级缓存:本地内存缓存,基于Caffeine实现,访问延迟在纳秒级别,无网络IO开销,适配热点不变数据
- L3级缓存:分布式缓存,Redis集群,访问延迟在毫秒级别,适配全量热点数据,支持分布式环境共享
6.2 本地缓存Caffeine实战
Caffeine是当前Java领域性能最高的本地缓存库,基于W-TinyLFU淘汰算法,命中率远高于Guava Cache,性能是Guava Cache的10倍以上,最新稳定版为3.1.8。
6.2.1 Caffeine集成与配置
首先引入依赖:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
配置类实现:
package com.example.config;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
public class CaffeineConfig {
// 本地缓存,存储热点用户数据
@Bean("userLocalCache")
public Cache<String, Object> userLocalCache() {
return Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.expireAfterAccess(2, TimeUnit.MINUTES)
.recordStats()
.build();
}
// 本地缓存,存储系统配置数据
@Bean("configLocalCache")
public Cache<String, Object> configLocalCache() {
return Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(60, TimeUnit.MINUTES)
.recordStats()
.build();
}
}
6.2.2 多级缓存使用示例
package com.example.service;
import com.github.benmanes.caffeine.cache.Cache;
import org.springframework.stereotype.Service;
import java.util.Random;
@Service
public class UserService {
private final Cache<String, Object> userLocalCache;
private final RedisClient redisClient;
private final UserMapper userMapper;
public UserService(Cache<String, Object> userLocalCache, RedisClient redisClient, UserMapper userMapper) {
this.userLocalCache = userLocalCache;
this.redisClient = redisClient;
this.userMapper = userMapper;
}
public UserVO getUserById(Long userId) {
String key = "user:" + userId;
// 1. 先查本地缓存
UserVO userVO = (UserVO) userLocalCache.getIfPresent(key);
if (userVO != null) {
return userVO;
}
// 2. 本地缓存未命中,查Redis缓存
userVO = redisClient.get(key, UserVO.class);
if (userVO != null) {
userLocalCache.put(key, userVO);
return userVO;
}
// 3. Redis未命中,查数据库
userVO = userMapper.selectById(userId);
if (userVO != null) {
// 缓存过期时间加随机值,避免缓存雪崩
redisClient.setEx(key, 300 + new Random().nextInt(100), userVO);
userLocalCache.put(key, userVO);
}
return userVO;
}
}
6.3 缓存三大异常场景的解决方案
高并发场景下,缓存穿透、缓存击穿、缓存雪崩是必须解决的核心问题,这里明确区分三个易混淆的场景:
| 场景 | 定义 | 根本原因 |
| 缓存穿透 | 大量查询不存在的数据,请求直接穿透缓存打到数据库 | 缓存和数据库中都无对应数据 |
| 缓存击穿 | 热点key过期,大量请求同时打到数据库 | 单个热点key过期,并发请求量大 |
| 缓存雪崩 | 大量key同时过期或Redis集群宕机,大量请求打到数据库 | 大量key同时过期,或缓存服务不可用 |
6.3.1 缓存穿透的解决方案
- 布隆过滤器:将所有存在的key存入布隆过滤器,请求先查布隆过滤器,若布隆过滤器判定不存在,则数据一定不存在,直接返回,无需查询缓存和数据库。Redis 7.4内置了RedisBloom模块,无需额外安装
- 空值缓存:对于数据库中不存在的数据,缓存一个空值到Redis,过期时间设置为30秒,避免重复查询数据库
布隆过滤器实战示例: 首先引入Redisson依赖(适配Redis布隆过滤器):
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.37.0</version>
</dependency>
代码实现:
package com.example.service;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
private final RedissonClient redissonClient;
private final UserMapper userMapper;
public UserService(RedissonClient redissonClient, UserMapper userMapper) {
this.redissonClient = redissonClient;
this.userMapper = userMapper;
}
// 项目启动时执行,初始化布隆过滤器
public void initUserBloomFilter() {
RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter("user_bloom_filter");
// 初始化布隆过滤器,预期数据量100万,误判率0.01%
bloomFilter.tryInit(1000000, 0.0001);
// 将所有用户ID存入布隆过滤器
List<Long> userIdList = userMapper.selectAllUserId();
for (Long userId : userIdList) {
bloomFilter.add(userId);
}
}
public UserVO getUserById(Long userId) {
RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter("user_bloom_filter");
// 布隆过滤器判定不存在,直接返回
if (!bloomFilter.contains(userId)) {
return null;
}
// 布隆过滤器判定存在,再查询缓存和数据库
// 后续缓存、数据库查询逻辑省略
return userMapper.selectById(userId);
}
}
6.3.2 缓存击穿的解决方案
- 热点key永不过期:对于核心热点key(比如首页热点商品),设置永不过期,后台异步更新缓存
- 互斥锁:热点key过期时,仅允许一个线程查询数据库并更新缓存,其他线程等待缓存更新完成后再查询,避免大量请求同时打到数据库,基于Redisson分布式锁实现
互斥锁实战示例:
public UserVO getUserById(Long userId) {
String key = "user:" + userId;
UserVO userVO = redisClient.get(key, UserVO.class);
if (userVO != null) {
return userVO;
}
// 缓存未命中,加分布式锁
String lockKey = "lock:user:" + userId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试加锁,等待时间3秒,锁过期时间10秒
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
try {
// 双重检查,避免加锁过程中其他线程已更新缓存
userVO = redisClient.get(key, UserVO.class);
if (userVO != null) {
return userVO;
}
// 查询数据库
userVO = userMapper.selectById(userId);
if (userVO != null) {
// 更新缓存,过期时间加随机值
redisClient.setEx(key, 300 + new Random().nextInt(100), userVO);
}
} finally {
lock.unlock();
}
} else {
// 加锁失败,等待100ms后重试
Thread.sleep(100);
return getUserById(userId);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return userVO;
}
6.3.3 缓存雪崩的解决方案
- 过期时间加随机值:给缓存过期时间添加随机偏移量,比如5分钟+0-100秒随机值,避免大量key同时过期
- Redis集群高可用:采用主从+哨兵+集群的部署模式,避免Redis单点故障,Redis 7.4集群模式支持自动分片、故障转移
- 服务熔断与限流:Redis集群不可用时,开启熔断,避免大量请求打到数据库,同时通过网关限流控制进入系统的请求量
- 多级缓存兜底:本地缓存可扛住大部分热点请求,即使Redis集群宕机,也能保证系统核心功能可用
6.4 Redis性能优化
Redis性能优化分为客户端优化、服务端优化、数据结构优化、持久化优化四个维度。
6.4.1 客户端优化
- 使用Lettuce客户端:Spring Boot 2.0+默认使用Lettuce,基于Netty的异步非阻塞客户端,性能远高于Jedis,支持连接池、集群、哨兵模式
- 连接池优化:设置合理的连接池参数,最大连接数设置为100左右,避免过多连接导致Redis服务端压力过大
- 批量操作:使用pipeline批量执行命令,减少网络IO次数,批量查询性能比多次单命令执行高10倍以上
- 避免大key:单个key的value不要超过10KB,大key会导致网络传输、序列化开销大,甚至阻塞Redis主线程
6.4.2 服务端配置
Redis 7.4优化配置(redis.conf):
daemonize yes
port 6379
bind 0.0.0.0
protected-mode no
requirepass your-redis-password
maxclients 10000
maxmemory 8g
maxmemory-policy volatile-lru
appendonly yes
appendfsync everysec
aof-use-rdb-preamble yes
rdbcompression yes
rdbchecksum yes
save 900 1
save 300 10
save 60 10000
tcp-keepalive 300
tcp-backlog 511
timeout 0
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
replica-lazy-flush yes
配置说明:
maxmemory:设置Redis最大内存,建议为物理内存的50%-70%,避免使用swap分区导致性能急剧下降maxmemory-policy:内存淘汰策略,volatile-lru对设置了过期时间的key使用LRU算法淘汰,适配大部分业务场景lazyfree-lazy-*:开启惰性删除,删除大key时使用后台线程异步删除,避免阻塞主线程appendfsync everysec:AOF持久化每秒同步一次,平衡性能与数据安全性,最多丢失1秒数据
6.4.3 数据结构优化
- 选择合适的数据结构:用户签到记录使用Bitmap,内存占用仅为Set的1/10000;排行榜使用SortedSet,性能远高于List排序
- 压缩列表优化:对于小的Hash、List、Set、SortedSet,Redis会使用压缩列表存储,内存占用更低,需合理设置压缩列表的最大元素个数和大小
- 禁用高危命令:禁止使用
keys *、hgetall、smembers等全量遍历命令,会阻塞Redis主线程,应使用scan命令分批遍历
6.4.4 持久化优化
- 开启RDB+AOF混合持久化:Redis 4.0+支持混合持久化,AOF文件包含RDB全量数据和增量AOF命令,重启恢复速度远快于纯AOF,同时保证数据安全性
- 避免业务高峰期执行RDB持久化:RDB持久化会fork子进程,fork时会阻塞主线程,内存越大阻塞时间越长,应在业务低峰期执行
- 关闭AOF自动重写,手动在低峰期执行:AOF自动重写可能在业务高峰期触发,导致Redis性能下降,建议关闭自动重写,每天凌晨低峰期手动执行
BGREWRITEAOF命令
七、数据库层性能优化
数据库是系统的最后一道防线,也是高并发场景下最容易出现性能瓶颈的环节。本文基于最新稳定版MySQL 8.4.2 LTS,从SQL优化、索引优化、事务优化、连接池优化、服务端优化五个维度,拆解全量优化方案。
7.1 MySQL核心架构与底层原理
MySQL分为Server层和存储引擎层,核心架构如下:
- Server层:包含连接器、查询缓存、解析器、优化器、执行器,负责MySQL核心服务功能,所有跨存储引擎的功能都在这一层实现
- 存储引擎层:负责数据的存储和提取,插件式架构,MySQL 5.5+默认使用InnoDB存储引擎,支持事务、行级锁、外键、崩溃恢复,是生产环境唯一选择
InnoDB存储引擎的核心是两个日志:redo log和binlog,这里明确区分两者的差异:
| 特性 | redo log | binlog |
| 所属层 | InnoDB存储引擎层 | Server层 |
| 核心作用 | 崩溃恢复,保证事务持久性 | 数据归档、主从复制 |
| 内容类型 | 物理日志,记录数据页的修改 | 逻辑日志,记录SQL原始逻辑 |
| 写入方式 | 循环写入,文件大小固定 | 追加写入,不会覆盖历史内容 |
| 生命周期 | MySQL重启后失效 | 永久存储 |
7.2 索引优化
90%的MySQL性能问题都是索引不合理导致的,InnoDB默认使用B+树索引,所有数据都存在叶子节点,叶子节点之间用双向链表连接,适配范围查询、排序、分组。
7.2.1 聚簇索引与非聚簇索引
这里明确区分两个易混淆的索引类型:
- 聚簇索引:即主键索引,叶子节点存储整行数据,InnoDB表必须有聚簇索引,一个表只能有一个聚簇索引
- 非聚簇索引:即二级索引,叶子节点存储主键的值,查询时先通过二级索引找到主键,再通过聚簇索引找到整行数据,这个过程称为回表,一个表可以有多个二级索引
7.2.2 索引设计的黄金法则
- 优先使用联合索引,避免多个单列索引:联合索引可覆盖多个查询条件,而多个单列索引MySQL只会选择其中一个最优的,无法同时使用
- 联合索引遵循最左前缀原则:查询必须从索引的最左列开始匹配,不能跳过中间列,否则后续列无法用到索引。比如联合索引
idx(a,b,c),a=? and b=? and c=?可使用全索引,a=? and c=?只能使用第一列,b=? and c=?完全无法使用索引 - 索引列不能参与计算、函数操作、隐式类型转换,否则会导致索引失效
- 尽量使用覆盖索引,避免回表:查询的列都在索引中,无需回表查询聚簇索引,性能提升10倍以上
- 控制索引数量:一个表的索引数量不要超过5个,索引会降低插入、更新、删除的性能,因为每次修改数据都要更新对应的索引
- 避免冗余索引:已有联合索引
idx(a,b),就无需再创建单列索引idx(a)
7.2.3 索引失效的常见场景
- 索引列使用函数操作、计算、表达式
- 索引列发生隐式类型转换
- 模糊查询以
%开头 - 使用
OR连接非索引列 - 联合索引不满足最左前缀原则
- 使用不等于(
!=、<>)、not in、is not null操作(大部分场景会导致索引失效) - MySQL优化器判断全表扫描比走索引更快时,会放弃索引
7.2.4 索引优化示例
首先创建用户表:
CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_name` varchar(50) NOT NULL COMMENT '用户名',
`phone` varchar(20) NOT NULL COMMENT '手机号',
`age` int NOT NULL COMMENT '年龄',
`gender` tinyint NOT NULL COMMENT '性别:1男,2女',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_phone` (`phone`),
KEY `idx_name_age_gender` (`user_name`,`age`,`gender`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
正确的查询示例,使用覆盖索引,无需回表:
EXPLAIN SELECT user_name, age, gender FROM user WHERE user_name = 'test' AND age = 20 AND gender = 1;
错误的查询示例,索引失效:
-- 索引列使用函数,索引失效
EXPLAIN SELECT * FROM user WHERE DATE_FORMAT(create_time, '%Y-%m-%d') = '2024-01-01';
-- 正确写法,使用索引idx_create_time
EXPLAIN SELECT * FROM user WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02';
-- 隐式类型转换,phone是varchar类型,传入数字导致索引失效
EXPLAIN SELECT * FROM user WHERE phone = 13800138000;
-- 正确写法,传入字符串,使用索引uk_phone
EXPLAIN SELECT * FROM user WHERE phone = '13800138000';
-- 模糊查询以%开头,索引失效
EXPLAIN SELECT * FROM user WHERE user_name LIKE '%test%';
-- 正确写法,前缀匹配,使用索引idx_name_age_gender
EXPLAIN SELECT * FROM user WHERE user_name LIKE 'test%';
7.3 SQL优化
SQL优化的核心原则是:减少扫描的行数,减少回表的次数,减少排序、分组的开销。
- 避免使用
select *,只查询需要的列,减少数据传输开销,更容易使用覆盖索引 - 避免使用子查询,尽量用join关联查询,MySQL 8.0+优化器已对子查询做了优化,但join性能更稳定
- 避免关联超过3个表,关联表过多会导致优化器选择错误的执行计划,性能下降
- 分页查询优化:offset很大时,
limit offset, size性能极差,因为MySQL会扫描前面的offset行再丢弃,优化方法是使用主键id过滤,比如select * from user where id > 100000 limit 10,性能比limit 100000, 10高100倍以上 - 避免在where子句中使用
or连接非索引列,会导致索引失效,可用union all代替 - 避免使用
order by rand(),会导致全表扫描后排序,性能极差
分页查询优化示例
错误的分页查询,offset很大时性能极差:
SELECT * FROM user ORDER BY id LIMIT 100000, 10;
正确的优化写法,使用主键id过滤:
SELECT * FROM user WHERE id > 100000 ORDER BY id LIMIT 10;
若id不连续,或排序字段不是id,可使用子查询先找到主键id,再关联查询:
SELECT u.* FROM user u
INNER JOIN (SELECT id FROM user ORDER BY create_time LIMIT 100000, 10) AS t ON u.id = t.id;
7.4 事务优化
InnoDB事务的ACID特性:原子性、一致性、隔离性、持久性,事务优化的核心是:缩小事务范围,减少锁的持有时间,避免死锁。
7.4.1 事务隔离级别
MySQL有4种事务隔离级别,默认是REPEATABLE READ(可重复读):
READ UNCOMMITTED:可读取其他事务未提交的数据,会出现脏读、不可重复读、幻读,生产环境绝对禁止使用READ COMMITTED:只能读取其他事务已提交的数据,避免了脏读,会出现不可重复读、幻读,大部分互联网公司使用该级别,锁范围更小,并发性能更高REPEATABLE READ:同一个事务内多次读取同一个数据的结果一致,避免了脏读、不可重复读,InnoDB通过MVCC和间隙锁解决了幻读问题,MySQL默认隔离级别SERIALIZABLE:所有事务串行执行,避免了所有问题,但并发性能极差,生产环境绝对禁止使用
生产环境建议使用READ COMMITTED隔离级别,锁范围更小,不会出现间隙锁,并发性能更高,同时binlog格式设置为row,不会出现主从复制不一致的问题。
7.4.2 事务优化的核心手段
- 尽量缩小事务范围,将非核心操作放到事务外面,比如日志记录、缓存更新、消息发送,减少事务执行时间和锁的持有时间
- 避免在事务中执行耗时的IO操作,比如远程调用、文件操作,会导致事务长时间不提交,锁一直持有,引发其他事务阻塞甚至死锁
- 避免在事务中循环操作数据库,比如循环插入数据,应使用批量插入,减少事务执行次数
- 合理设置事务超时时间,避免事务长时间不提交占用连接
- 避免长事务,长事务会导致undo log无法清理,占用大量磁盘空间,同时导致MVCC视图过旧,查询性能下降
7.4.3 事务优化示例
错误的事务写法,事务范围过大,包含远程调用:
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderCreateDTO dto) {
validateParam(dto);
// 远程调用商品服务,耗时IO操作
ProductVO productVO = productClient.getProductById(dto.getProductId());
// 远程调用库存服务,耗时IO操作
inventoryClient.deductStock(dto.getProductId(), dto.getQuantity());
// 插入订单数据
OrderDO orderDO = convertToDO(dto);
orderMapper.insert(orderDO);
// 发送消息、更新缓存
mqClient.sendOrderCreateEvent(orderDO);
redisClient.setEx("order:" + orderDO.getId(), 300, orderDO);
}
正确的事务写法,缩小事务范围,仅将数据库操作放在事务中:
public void createOrder(OrderCreateDTO dto) {
validateParam(dto);
// 远程调用放在事务外
ProductVO productVO = productClient.getProductById(dto.getProductId());
inventoryClient.deductStock(dto.getProductId(), dto.getQuantity());
// 仅数据库操作放在事务中
OrderDO orderDO = createOrderInTransaction(dto);
// 非核心操作放在事务外
mqClient.sendOrderCreateEvent(orderDO);
redisClient.setEx("order:" + orderDO.getId(), 300, orderDO);
}
@Transactional(rollbackFor = Exception.class)
public OrderDO createOrderInTransaction(OrderCreateDTO dto) {
OrderDO orderDO = convertToDO(dto);
orderMapper.insert(orderDO);
return orderDO;
}
7.5 连接池优化
Spring Boot默认使用HikariCP连接池,是当前Java领域性能最高的连接池,比Druid、C3P0性能高很多。
7.5.1 HikariCP配置
spring.datasource.url=jdbc:mysql://192.168.1.30:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=your-mysql-password
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=300000
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.leak-detection-threshold=60000
配置说明:
maximum-pool-size:最大连接数,Oracle官方推荐计算公式:connections = ((core_count * 2) + effective_spindle_count),8核CPU、SSD磁盘设置为20是合理的。连接数太多会导致CPU上下文切换频繁、锁竞争激烈,性能反而下降minimum-idle:最小空闲连接数,保持连接池中的最小空闲连接,避免请求到来时再创建连接leak-detection-threshold:连接泄漏检测阈值,设置为60秒,连接被占用超过60秒会打印警告日志,帮助排查连接泄漏问题
7.6 MySQL服务端优化
MySQL 8.4.2 LTS配置优化(my.cnf):
[mysqld]
port=3306
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
pid-file=/var/run/mysqld/mysqld.pid
user=mysql
default-storage-engine=InnoDB
character-set-server=utf8mb4
collation-server=utf8mb4_0900_ai_ci
max_connections=1000
wait_timeout=600
interactive_timeout=600
innodb_buffer_pool_size=12G
innodb_buffer_pool_instances=8
innodb_log_buffer_size=64M
innodb_max_undo_log_size=4G
innodb_log_file_size=2G
innodb_log_files_in_group=2
innodb_flush_log_at_trx_commit=1
sync_binlog=1
innodb_flush_method=O_DIRECT
innodb_read_io_threads=8
innodb_write_io_threads=8
innodb_io_capacity=2000
innodb_io_capacity_max=4000
innodb_autoinc_lock_mode=2
innodb_thread_concurrency=0
sql_mode=STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION
lower_case_table_names=1
slow_query_log=ON
slow_query_log_file=/var/log/mysql/slow.log
long_query_time=1
log_queries_not_using_indexes=ON
配置说明:
innodb_buffer_pool_size:InnoDB缓冲池大小,最核心的参数,用来缓存表的数据和索引,越大越好,建议设置为物理内存的50%-70%innodb_buffer_pool_instances:缓冲池实例个数,建议设置为与CPU核心数一致,减少锁竞争innodb_flush_log_at_trx_commit=1:事务提交时将redo log刷新到磁盘,保证事务持久性,生产环境必须设置为1sync_binlog=1:每次事务提交时将binlog刷新到磁盘,保证binlog不丢失,生产环境必须设置为1innodb_flush_method=O_DIRECT:使用直接IO,绕过操作系统缓存,避免双缓存,性能更高- 开启慢查询日志,超过1秒的SQL都会被记录,方便排查慢SQL问题
八、操作系统与硬件层优化
很多时候系统的性能瓶颈不是应用代码,而是操作系统和硬件配置不当,Linux操作系统优化分为CPU、内存、磁盘IO、网络四个维度。
8.1 CPU优化
- 关闭CPU节能模式,设置为性能模式,避免CPU自动降频导致性能下降
- 绑定CPU亲和性,将Java进程绑定到特定的CPU核心上,避免CPU上下文切换,提升缓存命中率,使用命令
taskset -cp 0-7 pid绑定Java进程到0-7号CPU核心 - 禁用NUMA内存交错,让进程优先使用本地内存,提升内存访问速度
8.2 内存优化
- 关闭swap分区,swap分区访问速度比物理内存慢几百倍,高并发场景下使用swap会导致系统性能急剧下降
- 设置
vm.swappiness=0,尽量不使用swap分区,仅当物理内存不足时才使用 - 设置
vm.overcommit_memory=1,允许内核过量分配内存,适配Java应用的堆内存预分配机制
8.3 磁盘IO优化
- 使用SSD固态硬盘,随机读写性能是机械硬盘的100倍以上,高并发场景必须使用SSD
- 调整磁盘IO调度算法,SSD使用
noop或none调度算法,机械硬盘使用deadline调度算法 - 关闭磁盘atime记录,在
/etc/fstab中设置noatime,nodiratime,避免每次访问文件都写入磁盘,减少IO开销 - 使用XFS文件系统,比ext4性能更高,支持更大的文件和分区,MySQL、Redis都推荐使用XFS
8.4 网络优化
Linux内核TCP参数优化(/etc/sysctl.conf):
net.ipv4.ip_forward = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_synack_retries = 2
net.ipv4.tcp_syn_retries = 2
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 0
net.ipv4.tcp_fin_timeout = 30
net.ipv4.tcp_keepalive_time = 300
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 3
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_max_tw_buckets = 200000
net.ipv4.tcp_max_orphans = 200000
net.core.rmem_default = 262144
net.core.rmem_max = 16777216
net.core.wmem_default = 262144
net.core.wmem_max = 16777216
net.core.netdev_max_backlog = 65535
net.core.somaxconn = 65535
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
net.ipv4.tcp_mtu_probing = 1
配置说明:
net.ipv4.tcp_syncookies=1:开启SYN cookies,防止SYN洪水攻击net.ipv4.tcp_tw_reuse=1:允许重用TIME_WAIT状态的连接,避免高并发下端口耗尽net.ipv4.tcp_tw_recycle=0:关闭TIME_WAIT快速回收,避免NAT环境下连接失败net.core.somaxconn=65535:调整监听队列大小,避免高并发下连接被拒绝net.core.netdev_max_backlog=65535:调整网络设备接收队列大小,避免高并发下数据包丢失
修改完配置后,执行sysctl -p命令生效。
九、全链路压测与瓶颈定位
所有的优化都必须基于压测和瓶颈定位,没有压测的优化都是盲目优化。本文采用最新稳定版JMeter 5.6.3进行压测,SkyWalking 9.7.0进行全链路监控,定位性能瓶颈。
9.1 压测方案设计
压测分为三个核心阶段:
- 基准压测:单接口压测,测试系统的基准性能,找到单接口的最大QPS和RT
- 混合场景压测:模拟线上真实业务场景,多个接口按比例混合压测,测试系统的整体性能
- 稳定性压测:长时间压测(比如72小时),测试系统的稳定性,排查内存泄漏、OOM、频繁GC等问题
压测核心关注指标:吞吐量(QPS)、响应时间(平均RT、P95、P99、P999 RT)、错误率、CPU利用率、内存利用率、磁盘IO、网络IO。
9.2 全链路监控与瓶颈定位
SkyWalking是国产开源全链路APM工具,完美支持Spring Cloud微服务架构,可监控从用户端到数据库的全链路请求,精准定位性能瓶颈。
瓶颈定位的核心步骤:
- 查看系统整体指标:QPS、RT、错误率是否达到预期
- 查看服务器资源指标:CPU、内存、磁盘IO、网络IO是否存在资源瓶颈
- 查看全链路追踪:找到请求的哪个环节耗时最长,是网关、服务、Redis还是数据库
- 针对性优化:慢SQL优化索引和SQL,Redis慢查询优化命令和数据结构,服务层耗时优化代码、线程池、JVM
9.3 性能优化的闭环
性能优化是一个持续的闭环过程:压测 -> 定位瓶颈 -> 优化 -> 再压测 -> 再定位,直到达到预期的性能目标,不要期望一次优化就能解决所有问题,需循序渐进、逐步迭代。
十、高并发优化的避坑指南与总结
10.1 易混淆技术点的明确区分
- 同步vs异步:同步是调用方等待结果返回再继续执行;异步是调用方不等待结果返回,结果通过回调通知返回
- 阻塞vs非阻塞:阻塞是调用方等待结果时线程被挂起,不占用CPU;非阻塞是调用方等待结果时线程不被挂起,继续执行其他任务,轮询检查结果
- 虚拟线程vs平台线程:核心区别是调度主体、内存占用、适用场景,虚拟线程适配IO密集型任务,平台线程适配CPU密集型任务
- 缓存穿透vs缓存击穿vs缓存雪崩:核心区别是触发场景和根本原因,穿透是查询不存在的数据,击穿是单个热点key过期,雪崩是大量key同时过期或缓存宕机
- 聚簇索引vs非聚簇索引:核心区别是叶子节点存储的内容,聚簇索引存储整行数据,非聚簇索引存储主键值,需要回表
10.2 高并发优化的常见坑
- 过早优化:无压测、无瓶颈定位的盲目优化,增加系统复杂度甚至引入bug
- 过度优化:为了提升1%的性能,大幅增加系统复杂度,导致维护成本飙升
- 单点优化:只优化数据库,忽略全链路其他环节,性能提升有限
- 忽略可用性:为了提升性能牺牲系统可用性,比如关闭事务持久化导致数据丢失
- 忽略监控:无监控无法验证优化效果,也无法定位系统瓶颈
- 不做压测验证:优化后不做压测,无法确认优化效果,甚至引入新的问题
10.3 总结
高并发系统的性能优化,从来都不是单点的技术炫技,而是端到端全链路的系统工程。从用户端发起请求的第一跳,到数据最终落盘的最后一环,每个环节都有优化的空间,每个细节都决定了系统的性能上限。优化的核心逻辑是:先通过压测和监控定位瓶颈,再针对性优化;先优化架构,再优化代码;先解决80%的性能损耗点,再打磨20%的细节,循序渐进,持续迭代。