Spring Boot 3.5 下数据库实践记录:HikariCP 调优、N+1 防护、事务管理与 Testcontainers 集成
目录
📦 本文基于的完整项目源码:https://github.com/meirongdev/shop
2026-04 实践更新 当前主线仓库里,MySQL 领域服务的集成测试基线已经基本统一为
@ServiceConnection;少数仍保留@DynamicPropertySource的测试,主要是因为除了数据库容器之外还需要额外挂接自定义属性,而不是数据库接线本身没有迁移完成。
在 Shop Platform 的 15 个服务中,11 个领域服务各自拥有独立的 MySQL 8.4 数据库。Spring Boot 3.5 结合 Spring Data JPA 3.5 提供了比较完善的数据库集成能力,但默认配置未必直接适合当前生产环境。需要额外说明的是:当前仓库已经稳定落地的是 ddl-auto: validate、Flyway 迁移、@ServiceConnection 集成测试,以及部分服务显式关闭 OSIV;像 Hikari 数值和 @EntityGraph 更适合作为“可参考实践”,不应直接写成“仓库现状”。
下面从六个维度整理一下 Spring Boot 3.5 下数据库实践里更常遇到的点。
HikariCP 连接池调优#
为什么需要调优#
Spring Boot 默认的 HikariCP 配置是 maximum-pool-size: 10。这个值适合当起点,但对容器化环境来说,既可能偏小,也可能偏大。
一个更稳妥的压测起点#
spring:
datasource:
hikari:
maximum-pool-size: 10 # 先作为压测起点
minimum-idle: 2 # 最小空闲连接
connection-timeout: 5000 # 获取连接超时(ms)
idle-timeout: 300000 # 空闲连接超时(5 分钟)
max-lifetime: 1200000 # 连接最大生命周期(20 分钟)
怎么确定 maximum-pool-size#
HikariCP 作者 Brett Wooldridge 的公式:
connections = ((core_count × 2) + effective_spindle_count)
对于云原生环境(单容器 1-2 CPU),这条公式更适合当成压测起点:
- 1 CPU 容器:通常先从 5 左右开始测
- 2 CPU 容器:通常先从 8-12 开始测
当前 shop 仓库并没有统一提交 spring.datasource.hikari.maximum-pool-size / minimum-idle 这类覆盖配置,所以更准确的结论应该是:仓库还没有把连接池数值调优固化下来,后续应依据实际的 DB RT、并发量和连接等待时间再定。
open-in-view: false 的影响#
什么是 OSIV#
Open Session In View(OSIV)是 Spring Boot 常见的默认行为——spring.jpa.open-in-view: true。它让 JPA Session 贯穿整个 HTTP 请求,包括视图渲染阶段。
为什么关掉它#
spring:
jpa:
open-in-view: false
OSIV 的问题:
- 隐藏 N+1 查询:在 Controller 中
lazy关联的实体被访问时才触发查询,开发者不易察觉 - 连接占用时间长:Session 从请求开始到视图渲染结束,数据库连接被长期持有
- 性能不可预测:视图层可能触发意外查询,响应时间不稳定
关掉后的替代方案#
关闭 open-in-view 后,延迟加载的关联在 Service 层之外访问会抛 LazyInitializationException。解决方案:
方案一:@EntityGraph(一种常见写法)
public interface OrderRepository extends JpaRepository<OrderEntity, String> {
@EntityGraph(attributePaths = {"items"})
Optional<OrderEntity> findByIdWithItems(String id);
}
方案二:JOIN FETCH
@Query("select o from OrderEntity o join fetch o.items where o.id = :id")
Optional<OrderEntity> findByIdWithItems(@Param("id") String id);
方案三:在 @Transactional 方法内完成数据加载
@Transactional(readOnly = true)
public OrderDetailView getOrderDetail(String orderId) {
OrderEntity order = orderRepository.findById(orderId).orElseThrow();
List<OrderItemEntity> items = order.getItems();
return new OrderDetailView(order, items);
}
当前 shop 仓库里能直接验证的是:auth-server 显式设置了 spring.jpa.open-in-view: false,而 order-service 等服务把响应组装放在事务边界内完成;仓库中暂时还没有可直接引用的 @EntityGraph 用法,所以这里更适合把 @EntityGraph / JOIN FETCH 当成后续应对复杂关联查询的常见模式,而不是现状描述。
参考:Spring Boot open-in-view Documentation、Vlad Mihalcea: open-in-view
@EntityGraph 防 N+1#
N+1 问题#
// ❌ N+1 查询:1 次查订单 + N 次查订单项
List<ShopOrderEntity> orders = orderRepository.findByBuyerId(buyerId);
for (ShopOrderEntity order : orders) {
order.getItems().size(); // 每次触发一次 SELECT
}
@EntityGraph 解决#
@EntityGraph(attributePaths = {"items"})
List<ShopOrderEntity> findByBuyerIdOrderByCreatedAtDesc(String buyerId);
生成的 SQL:
SELECT o.*, i.* FROM shop_order o
LEFT JOIN order_item i ON o.id = i.order_id
WHERE o.buyer_id = ?
ORDER BY o.created_at DESC
在常见场景下,这通常能把 N+1 收敛到一条 SQL。
@EntityGraph vs JOIN FETCH#
| 维度 | @EntityGraph | JOIN FETCH |
|---|---|---|
| 声明位置 | Repository 方法注解 | @Query 中写 JPQL |
| 灵活性 | 低(只能指定关联路径) | 高(可以写复杂查询) |
| 返回值 | 可以返回 Entity | 可能产生重复(需 DISTINCT) |
| 常见场景 | 标准 CRUD | 复杂查询 |
参考:JPA EntityGraph Documentation、Vlad Mihalcea: JPA Entity Graph
@Transactional 传播行为#
传播类型对比#
| 传播类型 | 行为 | 适用场景 |
|---|---|---|
REQUIRED(默认) |
加入现有事务,没有则新建 | 绝大多数场景 |
REQUIRES_NEW |
始终新建事务,挂起现有事务 | 日志记录、审计 |
NESTED |
嵌套事务(SavePoint) | 部分提交场景 |
NOT_SUPPORTED |
非事务执行 | 批量操作 |
NEVER |
不能在事务中 | 纯查询工具 |
MANDATORY |
必须在事务中 | 强制事务上下文 |
shop 项目当前更容易验证的事务实践#
@Service
public class OrderApplicationService {
@Transactional
public OrderApi.OrderResponse createOrder(OrderApi.CreateOrderRequest request) {
// 创建订单 + 写入订单项 + 写入 outbox
}
@Transactional(readOnly = true)
public List<OrderApi.OrderResponse> listOrders(OrderApi.ListOrdersRequest request) {
// 只读事务,完成订单和订单项的组装
}
}
readOnly = true 的作用:
- Hibernate 跳过脏检查(提升性能)
- MySQL 可能选择只读优化路径
- 文档意图:明确这个方法不修改数据
在当前仓库里,最常见的是 REQUIRED + readOnly = true 这组组合。REQUIRES_NEW 当然仍然是常见设计手段,但这篇文章更适合把它表述为“可选策略”,而不是“shop 仓库已经大量采用的事实”。
参考:Spring @Transactional Documentation、Propagation Behaviors
Testcontainers @ServiceConnection 集成#
传统测试的痛点#
// ❌ 旧方式:手动管理容器生命周期
@Testcontainers
@SpringBootTest
class OrderServiceTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.4")
.withDatabaseName("shop_order");
@DynamicPropertySource
static void properties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
}
}
@ServiceConnection(Spring Boot 3.1+)#
@Testcontainers
public abstract class AbstractMySqlIntegrationTest {
@Container
@ServiceConnection
protected static final MySQLContainer<?> MYSQL = new MySQLContainer<>("mysql:8.4")
.withDatabaseName("shop_order")
.withUsername("shop")
.withPassword("shop-secret");
}
这也是当前 shop 仓库里的真实写法:@ServiceConnection 自动把容器连接信息注入 Spring 上下文,不需要再手写 DynamicPropertySource。
Flyway 自动迁移#
测试启动时,Flyway 自动执行所有迁移脚本:
@Test
void contextLoads() {
// Flyway 已经执行了 V1__init.sql, V2__...
// 数据库 schema 是最新的
}
参考:Spring Boot Testcontainers、@ServiceConnection Documentation
Flyway 进阶实践#
基线迁移#
对于已有数据的数据库引入 Flyway 时:
-- V1__baseline.sql
-- 这是一个空迁移,标记当前 schema 状态
spring:
flyway:
baseline-on-migrate: true
baseline-version: "1"
repair-on-migrate#
spring:
flyway:
repair-on-migrate: true
当手动修改过 schema(如本地开发调试)时,Flyway 先修复 metadata 再执行迁移。注意:生产环境需谨慎使用。
多数据源#
每个服务只有一个数据源,但如果需要多数据源:
@Configuration
public class MultiDataSourceConfig {
@Bean
@Primary
Flyway primaryFlyway(@Qualifier("primaryDataSource") DataSource ds) {
return Flyway.configure().dataSource(ds).load();
}
@Bean
Flyway secondaryFlyway(@Qualifier("secondaryDataSource") DataSource ds) {
return Flyway.configure().dataSource(ds).load();
}
}
MySQL 8.4 特性应用#
MySQL 8.x(包括 8.4 LTS)已经稳定提供了几类对微服务很有价值的能力:
CTE(通用表表达式)#
-- 递归 CTE:查询订单及其所有子订单
WITH RECURSIVE order_tree AS (
SELECT id, parent_id, 1 AS level FROM shop_order WHERE id = ?
UNION ALL
SELECT o.id, o.parent_id, ot.level + 1
FROM shop_order o JOIN order_tree ot ON o.parent_id = ot.id
)
SELECT * FROM order_tree;
窗口函数#
-- 每个买家的订单按时间排名
SELECT buyer_id, order_no, total_amount,
ROW_NUMBER() OVER (PARTITION BY buyer_id ORDER BY created_at DESC) AS rn
FROM shop_order;
JSON 函数#
-- 从 JSON 列中提取字段
SELECT JSON_EXTRACT(config, '$.gameType') AS game_type
FROM activity_game;
参考与实现位置#
- Spring Boot DataSource Properties:https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties.data
- HikariCP Pool Sizing:https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing
- Spring Boot open-in-view:https://docs.spring.io/spring-boot/reference/data/sql.html#data.sql.jpa-and-spring-data.open-in-view
- Spring @Transactional:https://docs.spring.io/spring-framework/reference/data-access/transaction.html
- Spring Boot Testcontainers:https://docs.spring.io/spring-boot/reference/testing/testcontainers.html
- Flyway Versioned Migrations:https://documentation.red-gate.com/flyway/flyway-concepts/migrations/versioned-migrations
- MySQL 8.4 Release Notes:https://dev.mysql.com/doc/relnotes/mysql/8.4/en/
- Vlad Mihalcea: open-in-view:https://vladmihalcea.com/the-open-session-in-view-anti-pattern/
- Vlad Mihalcea: JPA Entity Graph:https://vladmihalcea.com/jpa-entity-graph/
- 仓库实现入口:
services/auth-server/src/main/resources/application.yml、services/order-service/src/main/resources/application.yml、services/order-service/src/main/java/dev/meirong/shop/order/service/OrderApplicationService.java、services/order-service/src/test/java/dev/meirong/shop/order/support/AbstractMySqlIntegrationTest.java
小结#
Spring Boot 3.5 下数据库实战的核心要点:
- HikariCP 调优:把 pool sizing 公式当成压测起点,尽量不要把某个数字当成通用最优值
- open-in-view: false:在我当前的实践里,关闭 OSIV 仍然值得优先评估,但当前仓库只在部分服务里显式配置
- N+1 防护:
@EntityGraph/JOIN FETCH是常见工具,但当前仓库还没把它们统一沉淀成主路径 - @Transactional 传播:当前仓库以
REQUIRED和readOnly = true为主,REQUIRES_NEW更适合作为补充策略 - Testcontainers @ServiceConnection:一行注解完成容器连接注入,Flyway 自动迁移
- Flyway 进阶:基线迁移、repair-on-migrate、多数据源
- MySQL 8.4:CTE、窗口函数、JSON 函数提升查询能力