知其然,知其所以然
Spring 全家桶 2026-10-20

Specification 与动态查询

实测 Spring Data JPA 4 的 Specification:and/or/not 的 SQL;where(null) 与 and(null) 不再允许,用 Specification.unrestricted();三个可选条件 8 种组合都是一条 SQL;分页的数据与 count 用同一份条件;带 join 的集合不加 distinct,分页会少人(内容 1 行、总数 1,实际 2);用户输入里的 % 是通配符,注入则被参数绑定挡住。

Spring Data JPA Specification 动态查询 Criteria 真实验证 动画演示
Spring 全家桶 2026-10-20

N+1 与 @EntityGraph、JOIN FETCH

实测 Spring Data JPA 的 N+1:200 个作者的 findAll 再遍历 books 发出 201 条 SQL,@ManyToOne 默认 EAGER 也是 201 条;join fetch、@EntityGraph、DTO 投影都是 1 条 SQL(25.9 ms 降到 7.5 ms);两个 List 一起 fetch 抛 MultipleBagFetchException,两个 Set 会得到 300 行笛卡尔积;hibernate.default_batch_fetch_size=50 把 201 条变成 5 条。

Spring Data JPA N+1 EntityGraph JOIN FETCH Hibernate 真实验证 动画演示
Spring 全家桶 2026-10-20

Open-In-View:数据库连接被占到响应结束

实测 Spring Boot 4.0.8 的 spring.jpa.open-in-view:默认打开并在启动时给出 WARN;打开时数据库连接从第一次查询一直被持有到请求结束(查库返回之后、睡 600 ms 之后活动连接都是 1);关闭后懒加载会在事务外抛 LazyInitializationException;池只有 2 个连接时,打开比关闭的吞吐低约 4.5 倍并出现 17 个超时 500。

Spring Data JPA Open-In-View HikariCP 连接池 真实验证 动画演示
Spring 全家桶 2026-10-20

@Version 乐观锁与重试

实测 JPA 的 @Version:每次更新是 where id=? and version=?,撞车时命中 0 行抛 ObjectOptimisticLockingFailureException;8 个线程各加 25 次,没有 @Version 少了 169、有 @Version 不重试放弃 169 次、重试后一个不丢(冲突 72 次);批量 JPQL 更新绕过 @Version,要写 update versioned;表单场景里丢掉旧 version 会静默覆盖。

Spring Data JPA 乐观锁 Version 重试 并发 真实验证 动画演示
Spring 全家桶 2026-10-20

审计:@CreatedDate 与 @LastModifiedBy

实测 Spring Data JPA 审计:@EnableJpaAuditing 加实体上的 @EntityListeners 与四个注解;更新时只改 last_modified_*;同值修改与只读不会更新审计字段,@Modifying 批量 update 会漏掉;AuditorAware 返回空时 createdBy 为 null 且不报错;换线程后 ThreadLocal 里的用户丢失;缺 @EnableJpaAuditing 或 @EntityListeners 都是静默不填。

Spring Data JPA 审计 Auditing CreatedDate 真实验证 动画演示
Spring 全家桶 2026-10-20

JdbcTemplate 与 NamedParameterJdbcTemplate:RowMapper 与批量

实测 Spring JdbcTemplate、NamedParameterJdbcTemplate 与 JdbcClient:queryForObject 的 0 行与多行异常;BeanPropertyRowMapper 对不上的列静默为 null;IN 空集合变成 in () 报语法错误;batchUpdate 在默认的 MySQL 参数下仍然发出 5000 条 INSERT(517 ms 对循环的 614 ms);update 的返回值是匹配的行数;忘了 RETURN_GENERATED_KEYS 时 INSERT 已经执行。

Spring JdbcTemplate NamedParameterJdbcTemplate RowMapper JdbcClient 批量 真实验证 动画演示
Spring 全家桶 2026-10-20

批量写入:saveAll、batchUpdate 与 rewriteBatchedStatements

实测 10000 行批量插入:默认参数下 batchUpdate 与循环一样发出 10000 条 INSERT;加 rewriteBatchedStatements=true 后 batchUpdate 变成 1 条多行 insert,45 ms;JPA saveAll 在 IDENTITY 主键下 batch_size 不起作用(10000 条 INSERT);手动主键不实现 Persistable 时 saveAll 每行先 select;手动主键加 Persistable 加 rewrite 后 94 ms、20 条 INSERT。

Spring Data JPA 批量写入 saveAll batchUpdate rewriteBatchedStatements 真实验证 动画演示
Spring 全家桶 2026-10-20

HikariCP 在 Boot 里的默认值,以及池耗尽时的症状

实测 Spring Boot 4.0.8 里 HikariCP 的默认值:最大 10、最小空闲 10、连接超时 30000 ms、最大寿命 1800000 ms、keepaliveTime 120000;池耗尽时请求等满 connection-timeout 后抛 CannotGetJdbcConnectionException;leak-detection-threshold 小于 2000 ms 会被直接关掉;池大小就是数据库并发度(2、4、10、32 个连接对应 92、176、413、1187 次/秒)。

Spring Boot HikariCP 连接池 池耗尽 泄漏检测 真实验证 动画演示
Spring 全家桶 2026-10-20

读写分离:AbstractRoutingDataSource 与事务内路由

实测 AbstractRoutingDataSource 做读写分离:直接用时 readOnly 事务也路由到了主库,因为取连接时 readOnly 标记还没设置;外面包一层 LazyConnectionDataSourceProxy 后 readOnly 事务才走从库;readOnly 外层里调 REQUIRED 的写方法被驱动拦住,调 REQUIRES_NEW 的写方法则走主库。

Spring JDBC 读写分离 AbstractRoutingDataSource 事务 LazyConnectionDataSourceProxy 真实验证 动画演示
Spring 全家桶 2026-10-20

Flyway:迁移顺序、校验和与失败后的状态

实测 Spring Boot 4.0.8 加 Flyway 11:迁移按版本号数值顺序执行(V1、V1_1、V2、V10);改了已执行的 V 脚本会因校验和不一致启动失败,R 脚本改了则重跑;迟到的小版本号脚本默认校验失败;MySQL 上脚本中途失败会留下半截的表与 success=0 的历史行,再次启动报 Detected failed migration;非空库第一次接入需要 baseline,基线版本默认为 1,V1 会被跳过。

Spring Boot Flyway 数据库迁移 校验和 真实验证 动画演示
Spring 全家桶 2026-10-20

Spring Data Redis:Lettuce、序列化器与 pipeline

实测 Spring Data Redis 4 与 Lettuce:默认 RedisTemplate 用 JDK 序列化,key 前面带 \xac\xed 头、value 161 字节而 JSON 只要 42 字节;64 个线程共用 1 个 Lettuce 连接约 95000 次/秒,开连接池要关掉共享连接才会起作用且更慢;10000 个 SET 循环 461 ms、pipeline 67 ms、MSET 10 ms;Redis 宕机时每个命令等满超时才失败,恢复后重连一次 1413 ms。

Spring Data Redis Lettuce 序列化 pipeline 真实验证 动画演示
Spring 全家桶 2026-10-20

Spring Cache + Redis:穿透、击穿、雪崩的配置位置

实测 Spring Cache 加 Redis:key 是「缓存名::key」且默认永不过期;cache-null-values 默认 true,null 也被缓存挡住穿透;sync = true 在默认的 Redis 缓存写入器下并不加锁(30 个线程执行 30 次),换成 lockingRedisCacheWriter 后只执行 1 次且跨进程有效;TTL 统一会同时过期,用 TtlFunction 可以打散。

Spring Cache Redis 缓存穿透 缓存击穿 缓存雪崩 真实验证 动画演示
京ICP备2026067502号-1京公网安备11010502063487号