性能優化!七個策略,讓Spring Boot 處理每秒百萬請求
環境:SpringBoot3.4.2
1. 簡介
在實施任何優化前,我首先明確了性能基準。這一步至關重要——若不清楚起點,便無法衡量進展,也無法定位最關鍵的改進方向。以下是我們的初始性能指標概況:
最大吞吐量:50,000 次請求/秒
平均響應時間:350 毫秒
95 分位響應時間:850 毫秒
峰值時段 CPU 使用率:85%-95%
內存占用:堆內存使用達可用空間的 75%
數據庫連接:頻繁達到連接池上限(100 )
線程池飽和:線程池資源經常耗盡
以上指標通過如下的工具進行收集所得:
- JMeter用于負載測試,確定基礎吞吐量數值
- Micrometer + Prometheus + Grafana實現實時監控與可視化
- JProfiler深入分析代碼中的性能熱點
- 火焰圖(Flame graphs)定位 CPU 密集型方法
根據上面的指標總結如下性能瓶頸:
- 線程池飽和默認的 Tomcat 連接器已達到性能上限
- 數據庫連接爭用HikariCP 連接池配置未針對實際負載優化
- 序列化效率低下Jackson 在請求/響應處理中消耗大量 CPU 資源
- 阻塞式 I/O 操作尤其在調用外部服務時表現明顯
- 內存壓力過度對象創建導致頻繁的 GC 停頓
接下來,我們將逐一的解決上面的問題。
2. 性能優化
2.1 使用響應式編程
阻塞方式:
@Service
public class ProductService {
private final ProductRepository productRepository ;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository ;
}
public Product getProductById(Long id) {
return repository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id)) ;
}
}
基于響應式改造:
@Service
public class ProductService {
private final ProductRepository productRepository ;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository ;
}
public Product getProductById(Long id) {
public Mono<Product> getProductById(Long id) {
return productRepository.findById(id)
.switchIfEmpty(Mono.error(new ProductNotFoundException(id)));
}
}
}
同時Controller層也需要改造
@RestController
@RequestMapping("/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService ;
}
@GetMapping("/{id}")
public Mono<ResponseEntity<Product>> getProduct(@PathVariable Long id) {
return service.getProductById(id)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
}
注意,對應依賴方面你需要引入如下相關的依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<!--基于響應式的mysql驅動包-->
<dependency>
<groupId>com.github.jasync-sql</groupId>
<artifactId>jasync-r2dbc-mysql</artifactId>
<version>2.1.24</version>
</dependency>
總結:僅這一項改動便使吞吐量翻倍,其核心在于更高效地利用線程資源。WebFlux 不再為每個請求分配獨立線程,而是通過少量線程處理海量并發請求。
有關響應式編程,請查看下面文章:
新一代WebFlux框架核心技術Reactor響應式編程基本用法
響應式編程引領未來:WebFlux與R2DBC的完美結合實現數據庫編程
SpringBoot3虛擬線程 & 反應式(WebFlux) & 傳統Tomcat線程池 性能對比
新一代web框架WebFlux到底要不要學?
2.2 數據庫優化
數據庫交互成為下一個關鍵性能瓶頸。我采用了三管齊下的優化策略:
- 查詢優化
我使用 Spring Data 的 @Query 注解取代了低效的自動生成查詢:
優化前:
List<Order> findByUserIdAndStatusAndCreateTimeBetween(
Long userId, OrderStatus status,
LocalDate start, LocalDate end) ;
優化后:
@Query("SELECT o FROM Order o WHERE o.userId = :userId " +
"AND o.status = :status " +
"AND o.createdDate BETWEEN :start AND :end " +
"ORDER BY o.createdDate DESC")
List<Order> findUserOrdersInDateRange(
@Param("userId") Long userId,
@Param("status") OrderStatus status,
@Param("start") LocalDate start,
@Param("end") LocalDate end) ;
使用 Hibernate 的 @BatchSize 優化 N+1 查詢:
@Entity
@Table(name = "t_order")
public class Order {
// ...
@OneToMany(mappedBy = "order", fetch = FetchType.EAGER)
// 批量抓取數據
@BatchSize(size = 30)
private Set<OrderItem> items ;
}
- 連接池優化
HikariCP 的默認設置造成了連接爭用。經過大量測試,我得出了這樣的配置(實際要根據自己的環境):
spring:
datasource:
hikari:
maximum-pool-size: 100
minimum-idle: 50
idle-timeout: 30000
connection-timeout: 2000
max-lifetime: 1800000
關鍵的一點是,連接數并不總是越多越好;這里的hikari可不支持響應式。所以,我們應該吧響應式與阻塞式2種方式進行分開處理。
基于響應式數據庫的配置如下:
spring:
r2dbc:
pool:
initialSize: 30
maxSize: 10
max-acquire-time: 30s
max-idle-time: 30m
- 使用緩存
對于頻繁訪問的數據添加了 Redis 緩存。
// 開啟
@Configuration
@EnableCaching
public class CacheConfig {
}
// 使用緩存
@Service
public class ProductService {
@Cacheable(value = "products", key = "#id")
public Mono<Product> getProductById(Long id) {
return repository.findById(id)
.switchIfEmpty(Mono.error(new ProductNotFoundException(id)));
}
@CacheEvict(value = "products", key = "#product.id")
public Mono<Product> updateProduct(Product product) {
return repository.save(product) ;
}
}
配置緩存:
spring:
cache:
type: redis
redis:
cache-null-values: false
time-to-live: 120m
需要更多個性化配置,可以自定義RedisCacheManager。
2.3 序列化優化
通過優化jackson序列化,可以明顯減少CPU的占用。
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper() ;
// 啟用 Afterburner 模塊以加速序列化
mapper.registerModule(new AfterburnerModule()) ;
// 僅僅序列化不為空的字段
mapper.setSerializationInclusion(Include.NON_NULL) ;
// 禁用不需要的功能
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) ;
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) ;
return mapper ;
}
}
如果對部分接口要求非常高,那么可以采用Protocol Buffers。
關于Protocol Buffers的使用,請查看下面文章:
接口優化!Spring Boot 集成 Protobuf 并使用 RestTemplate 實現微服務間通信
基于 Spring Boot 實現自定義二進制數據傳輸協議
2.4 線程池&連接優化
有了 WebFlux,我們需要調整 Netty 的事件循環設置:
spring:
reactor:
netty:
worker:
count: 32 #工作線程數(2 x CPU cores)
connection:
provider:
pool:
max-connections: 10000
acquire-timeout: 5000
對于使用 Spring MVC 的,調整 Tomcat 連接器:
server:
tomcat:
threads:
max: 200
min-spare: 50
max-connections: 8192
accept-count: 100
connection-timeout: 5000
這些設置使我們能夠以較少的資源處理更多的并發連接。
2.5 基于 Kubernetes 的橫向擴展:終極解決方案
通過橫向擴展提升系統容量。將應用容器化后部署至 Kubernetes 集群。
FROM openjdk:17-slim
COPY target/product-app.jar app.jar
ENV JAVA_OPTS="-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+ParallelRefProcEnabled"
ENTRYPOINT exec java $JAVA_OPTS -jar /app.jar
然后根據 CPU 利用率配置自動縮放:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: product-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
#目標 Deployment 的名稱(即需要被擴縮容的應用)
name: product-app
#副本數范圍限制5~20
minReplicas: 5
maxReplicas: 20
#定義觸發擴縮容的指標規則
metrics:
- type: Resource #使用資源指標(如 CPU、內存)
resource:
name: cpu #監控 CPU 資源使用率
target:
type: Utilization #指標類型為“利用率百分比”
#當持續超過 70% 時觸發擴縮容
averageUtilization: 70
利用 Istio 實施服務網格功能,以實現更好的流量管理:
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: product-vs
spec:
hosts:
- product-service # 目標服務名(需與 Istio 服務網格中注冊的名稱一致)
http: # 定義 HTTP 協議相關的流量規則(支持路由、重試、超時等策略)
- route: # 配置流量路由規則
- destination: # 指定流量的實際目的地
host: product-service # 目標服務名
retries: # 設置請求失敗時的重試策略
attempts: 3 # 最大重試次數(首次請求 + 3次重試 = 最多4次嘗試)
perTryTimeout: 2s # 單次請求(含重試)的超時時間(2秒無響應則中斷)
timeout: 5s # 整個請求(所有重試累計)的全局超時時間(超過5秒直接失敗)
這使我們能夠高效處理流量高峰,同時保持彈性。