优化数据库(微服务慢的往往不是 API,而是数据库:Spring Boot 数据库优化 9 招)

优化数据库(微服务慢的往往不是 API,而是数据库:Spring Boot 数据库优化 9 招)
微服务慢的往往不是 API,而是数据库:Spring Boot 数据库优化 9 招



很多时候我们以为“服务慢”是代码问题、线程问题、Pod 不够用——结果一路扩容、加实例、加线程,延迟还是在那儿。直到把链路压测和 DB 指标一对,你会发现真相很朴素:数据库扛不住了。在微服务里应用层可以水平扩,但数据库天然是共享瓶颈,“每一条不必要的 SQL 都会把问题放大”。

下面我按一个“从最常见、ROI 最高”到“更偏架构层”的顺序,把这套 Spring Boot 场景下的数据库优化清单整理成 9 步。你可以当成排查路径,也可以当成上线前的性能 Checklist。


先立一个第一性原理:DB 是共享瓶颈

微服务架构里你可以很轻松把应用实例从 3 个扩到 30 个,但你很难把数据库也同样线性扩展。于是一个很常见的现象是:

  • 应用扩容 → 并发更高
  • 并发更高 → SQL 次数/连接争用/锁等待一起上升
  • 最后表现出来:吞吐不升反降、P99 飙升、连接池排队

Step 1:先把连接池配“对”,而不是配“大”

Spring Boot 默认用 HikariCP,很多团队长期沿用默认最大连接数 10,然后在高并发下让请求线程排队等连接,吞吐直接塌。

我建议的做法:

  1. 先看现状:DB CPU、锁等待、慢 SQL、连接数峰值
  2. 再设池子:按“DB 能承受多少并发连接”来定,而不是按“服务线程数”来定
  3. 重点关注“连接等待时间”而不是“连接数看起来很大”

示例(思路参考即可):

spring:  datasource:    hikari:      maximum-pool-size: 30      minimum-idle: 10      connection-timeout: 2000      idle-timeout: 600000      max-lifetime: 1800000

⚠️ 关键提醒:连接池不是越大越快。超过 DB 承载只会带来更多上下文切换和锁竞争,整体更慢。




Step 2:干掉 N+1(它是“隐形性能杀手”)

典型场景:查订单列表,然后循环访问订单明细,ORM 每次触发一条 SQL。

List orders = orderRepository.findAll();for (Order o : orders) {  o.getItems().size(); // 又是一条 SQL}

100 个订单 → 101 次查询;到了高 RPS 场景直接炸。

优化数据库(微服务慢的往往不是 API,而是数据库:Spring Boot 数据库优化 9 招)

修法:

  • JPA:JOIN FETCH / EntityGraph
  • MyBatis:一次性 join 或 in 查询再组装
  • 只要原则不变:把“循环 SQL”变成“批量 SQL”

Step 3:只返回你需要的字段,别“整表搬家”

很多接口慢,不是查询慢,是拉了太多不必要的数据:大 JSON、BLOB、关联对象、冗余字段,最后还要序列化、再传输、再 GC。

用 DTO/Projection 只取必要列:

public record UserDto(Long id, String name) {}@Query("select new com.app.UserDto(u.id, u.name) from User u")List findUsers();

它带来的收益往往是“连锁反应”:网络 IO ↓、对象分配 ↓、序列化 ↓、GC 压力 ↓。


Step 4:索引不是玄学,用 EXPLAIN 把“猜”变成“证据”

我见过太多“接口突然慢了”的根因,最后就是:缺索引 / 索引没命中 / 查询计划退化

习惯性做两件事:

  • EXPLAIN ANALYZE ...
  • 看是否出现 Seq Scan(全表扫描信号之一)

如果你在计划里看到 Seq Scan,意味着数据库在逐行扫描并过滤,通常需要考虑合适索引或改写查询。


Step 5:避免“话痨式微服务”——一次请求串 5 次 DB 往返

坏味道长这样:

  • A 查自己 DB
  • A 同步调 B → B 查自己 DB
  • B 再同步调 C → C 查自己 DB
  • 一次请求变成 5 次 DB round trip,延迟指数级上升

优化方向(按优先级):

  • 能在本服务一次查完的,不要拆成多服务链路
  • 必须跨服务:做 API Composition,但控制同步链深度
  • 稳定数据:缓存/本地缓存/只读副本
  • 强一致要求不高:事件驱动(最终一致)来换性能与韧性

Step 6:事务边界要短——批处理要“分段提交”

把一个大循环放进一个 @Transactional,最容易带来:

  • 长事务 → 锁持有时间长
  • 回滚代价大
  • 写入吞吐下降

更推荐:

  • 按 chunk 分段事务
  • 写入走 batch

Spring Boot 配 Hibernate batching 要用正确的属性名:
spring.jpa.properties.hibernate.jdbc.batch_size


Step 7:减少锁竞争——热点行/热点计数器是“扩展性天敌”

例如同一库存行被 500 并发更新:

update inventory set stock = stock - 1 where id = 10;

锁队列会把吞吐拖死。

常用解法:

  • 乐观锁(版本号)
  • 热点拆分(分桶、分片、按业务维度拆行)
  • 避免共享计数器(用分段计数/异步汇总)
  • 写入改事件驱动,削峰填谷



Step 8:别只盯 API 延迟,要盯“DB 真实瓶颈指标”

我通常会在看板上固定这些:

  • DB CPU / load
  • active connections(活跃连接数)
  • lock wait time(锁等待)
  • slow query log(慢查询)
  • Hikari connection wait(连接等待时间)

如果连接等待长期 > 100ms,基本可以判定:瓶颈在 DB 或 SQL


Step 9:缓存要“有边界”,否则就是“慢性事故”

缓存能极大缓解 DB 读压力,但前提是:有 TTL、有最大容量、有失效策略

例如 Caffeine:

Caffeine.newBuilder()  .maximumSize(50_000)  .expireAfterWrite(10, TimeUnit.MINUTES)  .build();

文中给的量级是:稳定读场景缓存能显著降低 DB 负载(可到 60%–80%)。我更保守的说法是:只要缓存命中率做起来,DB 压力下降通常非常明显


最后一段:微服务的“扩容幻觉”

微服务确实能让团队更独立、发布更快,但它也天然增加网络调用、DB 调用和系统复杂度。如果 DB 访问不优化,扩应用实例往往只是把 DB 压垮得更快

所以我现在做性能优化会优先问自己一句话:

这一波优化,能不能让“每个请求的 SQL 次数更少、每条 SQL 更快、每次写入锁更短、每次读更可复用”?

能做到这四点,你会发现:很多时候甚至不用扩容,系统就能多扛一倍。


如果你是 Java / 后端 / 全栈开发者,关注我一起走。

本号持续输出:

  • Java 核心 & 高并发 & JVM
  • Spring / 架构设计 / 实战踩坑
  • JavaScript/TS/Vue/大前端
  • AI × 编程 / Agent × 工程化

关注我,少踩坑,多走捷径。欢迎点赞 、收藏 ⭐、转发给需要的朋友

文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有

相关阅读

最新文章

热门文章

本栏目文章