接口性能优化是后端开发的高频话题,也是区分初级与高级工程师的重要分水岭。性能问题通常不是某一处代码写错了,而是由“重复的 I/O 消耗”“同步阻塞等待”和“资源创建/销毁开销”共同导致的。本节聚焦三种最实用、最落地的优化手段:批量操作、异步处理与池化技术,它们分别从减少交互次数、释放线程资源和复用昂贵对象三个维度提升系统吞吐量。
21.4.1 批量操作:减少网络往返与数据库交互
绝大多数接口的性能瓶颈不在 CPU,而在 I/O 调用次数。一次远程调用或数据库查询本身可能只需要几毫秒,但当循环中逐条执行时,累积的时延将呈线性增长。
1. 避免循环中的单条查询
典型反模式:根据 ID 列表逐个查询用户信息。
// 错误:N+1 问题
public List<OrderVO> getOrders(List<Long> orderIds) {
return orderIds.stream()
.map(id -> {
Order order = orderMapper.selectById(id);
User user = userMapper.selectById(order.getUserId()); // 每次循环一次DB
return assembleVO(order, user);
})
.collect(Collectors.toList());
}
优化方式:一次性传入 ID 列表,使用 IN 查询,然后在内存中组装。
public List<OrderVO> getOrders(List<Long> orderIds) {
List<Order> orders = orderMapper.selectBatchIds(orderIds);
List<Long> userIds = orders.stream().map(Order::getUserId).distinct().collect(Collectors.toList());
List<User> users = userMapper.selectBatchIds(userIds);
Map<Long, User> userMap = users.stream().collect(Collectors.toMap(User::getId, Function.identity()));
return orders.stream()
.map(order -> assembleVO(order, userMap.get(order.getUserId())))
.collect(Collectors.toList());
}
对于 MyBatis-Plus 等 ORM,可直接使用 selectBatchIds。如果是 JPA,利用 findAllById。关键原则:能用一条 SQL 查出来的数据,绝不分多条。
2. 批量插入与更新
逐条 INSERT 的场景在数据导入、日志采集、消息消费时非常普遍。MySQL 支持的多行插入可大幅降低与数据库的通信次数。
// 错误:逐条插入
for (LogRecord record : records) {
logMapper.insert(record);
}
// 正确:批量插入
logMapper.insertBatch(records);
MyBatis 中批量插入可通过动态 SQL 的 <foreach> 实现,或使用 ExecutorType.BATCH 模式:
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
LogMapper mapper = sqlSession.getMapper(LogMapper.class);
for (LogRecord record : records) {
mapper.insert(record);
}
sqlSession.commit();
JPA 同样支持 saveAll() 进行批量持久化,但需注意开启 hibernate.jdbc.batch_size 参数并关闭批量 ID 生成策略,否则可能退化为逐条 INSERT。
3. 远程调用合并
当微服务间聚合接口需要调用多次下游服务时,考虑提供 批量查询端点,或使用 GraphQL 类技术一次获取多个资源。无法改变下游时,可在调用方使用 CompletableFuture 并行调用(见下一小节),但这仍然受制于连接数和下游 QPS,批量接口才是根本解法。
// 聚合接口原本需要调用 10 次获取用户信息
// 要求下游提供 /users?ids=1,2,3,4... 批量接口
List<User> users = userClient.batchQuery(userIds);
21.4.2 异步处理:释放主线程,提升吞吐量
同步阻塞是 Web 容器线程池耗尽的主因。当一个请求处理中包含耗时的 I/O 操作(远程调用、文件读写、邮件发送),而线程又傻等结果时,该线程就无法服务其他请求。
1. 非核心逻辑异步化
并非所有操作都需要让用户同步等结果。邮件通知、短信、日志、数据同步等“可延迟”逻辑,应该从主流程中剥离,通过消息队列或 @Async 异步执行。
Spring 提供了简易的异步支持:
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}
@Service
public class NotificationService {
@Async
public void sendWelcomeEmail(User user) {
// 耗时邮件发送
}
}
在 Controller 或 Service 中,调用 sendWelcomeEmail 后主线程立即返回,邮件由异步线程池处理。但要注意:
@Async方法必须是public,且不能在同一个类内部调用(否则无法通过代理走 AOP)。- 异常不会被主线程捕获,需要自定义
AsyncUncaughtExceptionHandler记录日志。 - 线程池配置必须合理,结合拒绝策略(通常
CallerRunsPolicy或直接抛异常并监控)。
2. 并行调用,缩短响应时间
聚合层经常需要同时拉取多个下游服务的数据。如果串行调用,总耗时是 sum(t1, t2, t3),而并行调用可以缩短为 max(t1, t2, t3)。
使用 CompletableFuture 实现:
public AggregatedVO aggregateData(Long userId) {
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> userService.getUser(userId));
CompletableFuture<List<Order>> orderFuture = CompletableFuture.supplyAsync(() -> orderService.getUserOrders(userId));
CompletableFuture<Profile> profileFuture = CompletableFuture.supplyAsync(() -> profileService.getProfile(userId));
CompletableFuture.allOf(userFuture, orderFuture, profileFuture).join();
User user = userFuture.join();
List<Order> orders = orderFuture.join();
Profile profile = profileFuture.join();
return new AggregatedVO(user, orders, profile);
}
这里需要为 supplyAsync 指定自定义线程池,否则会使用公共的 ForkJoinPool,在容器环境下可能导致所有请求共享同一个池,相互影响。包装一个通用并行工具类是常见做法。
3. 响应式与非阻塞 I/O
对于网关或高并发代理服务,全程非阻塞是更高阶的优化方向。Spring WebFlux 利用 Reactor 模型,让少量线程处理海量连接,线程不会因等待 I/O 而被阻塞。但响应式与 JDBC 等阻塞技术天然不兼容,需搭配 R2DBC、Reactive Redis 等整套生态,改造成本较高,一般优先考虑批量与异步即可解决大部分场景。
21.4.3 池化技术:复用昂贵对象,降低延迟
创建连接、线程这种重量级对象的开销不可忽视(TCP 握手、数据库认证、线程栈内存分配)。池化技术通过预创建一批对象并复用,避免了频繁的创建和销毁,提升响应速度并降低系统负载。
1. 数据库连接池(HikariCP)
Spring Boot 2.x 默认使用 HikariCP,它以极简设计和卓越性能著称。通常只需在配置文件中调优关键参数:
spring:
datasource:
hikari:
maximum-pool-size: 20 # 最大连接数,按应用并发量调整
minimum-idle: 10 # 最小空闲连接,防止冷连接开销
idle-timeout: 600000 # 空闲连接超时 10 分钟
connection-timeout: 30000 # 等待连接的最大时间
max-lifetime: 1800000 # 连接最大存活时间,应小于数据库连接超时
leak-detection-threshold: 10000 # 连接泄露检测,10秒未归还则告警
关键实践:
- 连接池大小不宜过大(一般 CPU 核数 * 2 + 有效磁盘数)。
- 监控连接池的使用率(HikariCP 提供 JMX 指标),避免连接耗尽。
- 在事务中尽早归还连接,配合
@Transactional的只读参数减少锁开销。
2. HTTP 连接池
使用 RestTemplate 或 WebClient 发起 HTTP 调用时,如果不配置连接池,默认实现每次新建连接,负载高时会导致大量 TIME_WAIT 和端口耗尽。应在构造时绑定 HttpComponentsClientHttpRequestFactory 并配置 PoolingHttpClientConnectionManager。
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(200); // 全局最大连接数
cm.setDefaultMaxPerRoute(50); // 每个路由最大连接数
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(cm)
.evictExpiredConnections() // 自动清除过期连接
.build();
RestTemplate restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient));
对于 Spring Cloud OpenFeign,可通过 feign.httpclient.enabled=true 开启 Apache HttpClient 连接池,并调整 feign.httpclient.max-connections 等参数。
3. 线程池复用
在上一节异步处理中已经涉及线程池的配置。最佳实践是根据业务分离线程池:例如,邮件异步任务使用一个专用线程池避免影响短信发送任务;定时任务使用 ThreadPoolTaskScheduler 而非 new Thread()。
Spring 提供的 ThreadPoolTaskExecutor 是 ExecutorService 的易配置封装,务必通过 @Bean 显式创建,并为其设置可读的名称前缀,方便监控排查。
@Bean("orderAsyncExecutor")
public Executor orderAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("order-async-");
return executor;
}
使用时通过 @Async("orderAsyncExecutor") 指定。
21.4.4 真实的权衡与监控闭环
上述三种技术并非“银弹”,不加思考地滥用反而引入新问题:
- 批量操作导致的单次 SQL 过大、锁持有时间变长,需分批处理并控制每批大小。
- 异步处理带来的数据一致性问题、事务原子性破坏(邮件发送成功但事务回滚),需要本地消息表或事务消息来保障最终一致性。
- 池化技术若配置不当,可能出现连接泄漏、队列无限增长导致的 OOM。
性能优化的所有决策都必须建立在测量的基础上。在生产环境中接入指标监控(如 Micrometer + Prometheus)观测线程池使用率、连接池等待时间、慢 SQL 次数和接口响应分位数,形成“测量→优化→验证”的闭环。这样才能让优化真正落地,而不是盲人摸象式的猜想。
至此,性能优化的几个关键维度已经覆盖。下一节将从缓存角度切入,探讨如何利用本地缓存和分布式缓存进一步降低数据库压力,提升整体响应能力。