数据库连接池调优 _Spring Boot 性能优化 _ 服务器 SpringBoot 性能调优

初次接手一个 Spring Boot 项目之际,功能运行顺畅、测试结果全部呈绿色,内心还算比较安稳。

可一到压测环境,请求就像掉进蜂蜜里,延迟直线飙升。

在那之后,我才终于弄明白,Spring Boot 其自身本来并非是慢的状态,而变慢的缘由在于,是我们将各种各样的那所谓“默认值”,给错误地当作了“最佳实践”呀。

对于这篇内容,我会依据自身所经历的那些坑,从数据库方面,线程池方面,序列化方面,再到 JVM 方面,将常见的性能瓶颈逐个拆开来讲

每一节,都会给出,能直接落地的,检查动作,你拿去,对照自己的项目,做一轮体检,效果,往往,比你想象的明显。

0. 别凭感觉,先把基线立起来

优化前如果连当前性能数据都没有,你永远只能“感觉快了”。

最起码要做三件事情:其一,对一个核心接口展开压测,将平均响应时间以及吞吐量记录下来;其二,对 CPU 以及内存使用率进行监控;其三,把接口的详细日志打开,去查看每个环节所耗费的时间。

要是你当下尚未具备“应用内部视角”,那么优先将 Actuator 与 Micrometer 以及 Prometheus/Grafana 连接起来,使其相互接通利用

有了这些数据,你才知道改哪里有效,而不是盲目调参数。

1. 数据库 N+1 查询:最隐蔽的杀手

列表接口所返回的是总计 100 条的用户数据,对于其中每一条用户而言,均需要去查询包括订单、角色、标签等相关信息,代码之中明确写着 getOrders(),然而最终的结果却是,Hibernate 悄然无息地打出了 1 次查询用户以及 N 次查询订单的 N 加 1 这样的问题。

解决的思路并非是将@OneToMany 全部更改成为 EAGER,如此这般会引发更为众多的混乱状况。

@Entity
@NamedEntityGraph(
  name = "User.orders",
  attributeNodes = @NamedAttributeNode("orders")
)
class User { ... }
@EntityGraph("User.orders")
List findAllWithOrders();

比起其他方式,我更为推荐依据场景来挑选工具:借助 EntityGraph 以声明的方式明确某次查询要带上关联;当存在较强控制需求的时候,直接采用 JOIN FETCH要是不得不使用懒加载,那么能够通过设置 BatchSize 达成批量懒加载

一个判断的标准是,将 SQL 日志打开,去查看一次请求究竟产生了多少条查询语句。

@Query("select u from User u join fetch u.orders where u.id in :ids")
List findAllWithOrders(@Param("ids") List ids);

2. 连接池太小:吞吐量的阀门

@OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
@BatchSize(size = 25)
private List orders;

有不少人认为,HikariCP 自身带着便无需去管,然而在线上高峰时段,出现了“连接获取超时”的情况。

默认池的大小通常较为保守,在那种并发请求众多的情况下,以及每个请求都得对数据库进行操作的系统当中,连接池成为吞吐量的控制部件。

存在这样一个可供使用的起始点,它表现为将 maximumPoolSize 从 10 调整为 50,然而这并非是越大就会越好的情况。

更加务实的一种策略是,首先进行压测,查看“等待连接”的比例如何,接着再逐步将其调大,直至达到等待基本消失,然而数据库负载却依旧保持稳定的那个点。

池子过大,数据库反而会因连接切换开销而变慢。

3. 缓存乱用:加了反而更慢

spring:
  datasource:
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      connection-timeout: 20000
      idle-timeout: 300000
      max-lifetime: 1200000
      leak-detection-threshold: 60000

曾经我也做过这样的事,那就是,把@Cacheable 添加到每一个查询数据库的方法上,最终却发觉,缓存命中率很低,而且还无端地占据了内存。

数据库连接池调优 _ 服务器 SpringBoot 性能调优 _Spring Boot 性能优化

用以空间换取时间作为缓存的本质,其前提条件是数据被频繁读取并且变化不频繁。

我的规则是很朴素的,是只去缓存那般内容的,那般内容是要同时满足“读远多于写”这个条件的,以及要满足“数据允许短暂不一致”这个条件的。

另外,别把派生字段单独再缓存一次,应该复用主对象缓存。

举例来说,倘若用户信息缓存之中已然含有了昵称以及头像,那么便没有必要再去单独缓存“用户昵称”这个字段。

4. JSON 序列化:返回了整个数据库

直接将 JPA Entity 当作 API 响应予以返回,Jackson 常常会对所有关联对象进行序列化,甚至还会引发懒加载查询

数据库连接池调优 _ 服务器 SpringBoot 性能调优 _Spring Boot 性能优化

解决方式很简单:使用DTO

DTO 不只是为了性能,它更是API契约隔离层。

你可以只选择需要的字段,避免把整个实体树暴露出去。

存在这样一种情况,有一个接口,它返回了 20 个字段,然而前端仅仅使用其中 5 个,如此一来,这便是在造成带宽以及序列化时间的浪费

5. 异步处理:别让用户等非关键路径

在注册接口当中,最为常见的那些慢操作,包括发邮件,进行打点,发送 MQ,刷新缓存等等,像这些对于主流程成功并不会产生影响的动作,是不应该去阻塞响应的。

record UserSummaryDTO(Long id, String name) {}
@GetMapping("/users/{id}/summary")
UserSummaryDTO summary(@PathVariable Long id) {
  var u = userService.findById(id);
  return new UserSummaryDTO(u.getId(), u.getName());
}

将 Spring 的@Async 与明确的线程池相结合,这是最为简便的能够实现提速的方式

要是你所运用的是 Java 21 以及较新的版本的 Spring Boot,那么虚拟线程也是值得去进行评估的,然而千万别将它视作银弹

异步边界、IO依赖、数据库连接池大小依然决定吞吐量上限。

6. 启动慢:别让 Spring 全加载

@EnableAsync
@Configuration
class AsyncConfig {
  @Bean("appExecutor")
  Executor appExecutor() {
    var ex = new ThreadPoolTaskExecutor();
    ex.setCorePoolSize(5);
    ex.setMaxPoolSize(10);
    ex.setQueueCapacity(200);
    ex.setThreadNamePrefix("async-");
    ex.initialize();
    return ex;
  }
}
@Async("appExecutor")
public void sendWelcomeEmail(...) { ... }

项目规模一旦变大,启动速度迟缓情况往往源于两件事情,其一为组件扫描所涵盖的范围过于宽泛,其二是程序启动之际对所有单例 Bean 进行了初始化运作。

三个能迅速见到效果的动作:将扫描包进行缩小,仅扫描必需的路径;慎重开启懒加载,然而要留意首次调用时会变缓慢;对于大项目而言,可以采用 spring - context - indexer,它在构建期间预先进行候选组件的计算,以此来减少运行时期的扫描以及反射所产生的开销

要是你处于那种有着诸多小服务,具备弹性伸缩特性,且对冷启动颇为敏感的场景之中,那么 GraalVM Native Image 所带来的收益是极为直观的,不过呢,得去接纳构建时间以及兼容性成本。

7. JVM 参数:别等 GC 报警才动手

很多“内存问题”本质上是GC在求救。

一个能够被使用的起始点示例为:-Xms2g ,-Xmx2g ,-XX:+UseG1GC ,-XX:MaxGCPauseMillis=200。

@SpringBootApplication(scanBasePackages = {
  "com.xxx.controller",
  "com.xxx.service",
  "com.xxx.config"
})
class App {}

避免因扩容产生开销,要固定堆大小,使用 G1 垃圾回收器,还要设置合理的暂停目标。

spring:
  main:
    lazy-initialization: true

然后观察GC日志,看Full GC是否频繁发生。

如果年轻代频繁回收,可以适当调整-XX:NewRatio

不要去相信那所谓的“默认就是最好的”,JVM 的默认堆大小常常是不契合你的业务的。

8. 生产配置:性价比最高的优化

很多性能提升不是改代码,而是改几行配置。

比如说,Tomcat 线程池的大小规格这一情况,Hibernate 批处理的大小规模这种情形,以及将日志级别调整为 WARN 了这一行为

尤其是日志,在压测时打印大量DEBUG日志会直接拖垮IO

java -jar app.jar 
  -Xms512m -Xmx2g 
  -XX:+UseG1GC 
  -XX:MaxGCPauseMillis=200 
  -XX:+UseStringDeduplication

还有 Spring 当中的 jackson 配置,将 FAIL_ON_UNKNOWN_PROPERTIES 关闭,如此一来,也能够减少那些不必要的校验开销

这些配置改动几分钟就能完成,收益却很明显。

server:
  tomcat:
    threads:
      max: 200
      min-spare: 10
    connection-timeout: 20000
    accept-count: 100
spring:
  jpa:
    properties:
      hibernate:
        jdbc:
          batch_size: 20
        order_inserts: true
        order_updates: true
    show-sql: false
logging:
  level:
    org.springframework: WARN
    org.hibernate: WARN
    com.yourapp: INFO

性能实现优化的关键核心并非在于掌握住数量多少的技巧,而是要做到将每一个“ 默认值” 都视作是能够去质疑的对象。

Spring Boot 助力将项目运行起来,然而,至于要跑得快速、跑得稳健、跑得节省,则需依赖你去领会它默认所做之事、何时对你不合适、以及你凭借数据证实改变是具备成效的。

供你参考的是,手持这 8 个检查点,从中选一个最令你起疑的接口着手开启行动,率先进行压力测试,接着修改配置,之后再度施压测试,以数字作为依据来表明情况。