bigywater.tech
Java · 417

JPA 一级缓存:查询为什么可能不再访问数据库

“一级缓存让查询不再访问数据库”只对一小部分操作成立,而且它带来的陈旧数据和内存问题常被忽略。这篇用真实的 SQL 计数、另一个连接的并发修改和 10 万行的堆内存测量,量出它的行为。

+0 / +1
同一个 EntityManager 里第二次 find 与同一行的 JPQL 查询新增的 SQL 数
30 vs 99
另一个连接已把 age 改成 99 后,JPQL 重新查询返回的实体的 age(缓存里的旧值)与投影查询读到的 age
+82 MiB vs +0
读 100,002 行不清理缓存与每 1000 个 em.clear() 的堆内存增量

哪些操作命中缓存

实测 · 本机执行输出
  find(1) 第一次:+1
  find(1) 第二次:+0(命中一级缓存)
  再 find 两次:+0
  JPQL 按主键查同一行:+1(JPQL 总是访问数据库)
  JPQL 按唯一字段 code 查同一行:+1
  getReference(1)(已经在缓存里):+0
  find 一个不存在的主键(999)三次:+3(不存在的行没有“负缓存”,每次都查)
  count 聚合查询:+1
  先用 JPQL 把 bob 查出来,再 find(2):+0,q == q2 为 true(JPQL 的结果也会放进一级缓存)

陈旧数据

实测 · 本机执行输出
  加载 alice:age = 30,name = alice
  (另一个连接已经提交:age = 99,name = ALICE-DB)
  find(1):age = 30,name = alice(缓存里的旧值)
  JPQL 重新查询(SQL 确实发给了数据库):返回对象 == 缓存里的对象为 true,z.age = 30,z.name = alice(数据库里已经是 99,但返回的还是缓存里的旧实体)
  只查字段(投影)select p.age, p.name:99, ALICE-DB(投影直接读数据库,看到新值)
  em.refresh(x) 之后:age = 99,name = ALICE-DB
  em.clear() 后再 find:age = 100(重新从数据库加载)

丢失更新

实测 · 本机执行输出
  EntityManager 1 加载 alice:age = 10
  (另一个连接把 age 改成 77 并提交)
  EntityManager 1 只改了 name,提交之后库里:age = 10,name = alice-renamed(UPDATE 带上了所有列,把另一个事务写的 age = 77 覆盖回了 10)

内存

实测 · 本机执行输出
  加载前堆内存 10 MiB
  托管实体不清理(一个 EntityManager 全部留着):读完 100002 行,堆内存 92 MiB(+82 MiB),耗时 723 ms,em.contains 随机一个 = true
  每 1000 个 em.clear():读完 100002 行,堆内存 10 MiB(+0 MiB),耗时 526 ms
  StatelessSession(没有一级缓存):读完 100002 行,堆内存 10 MiB(+0 MiB),耗时 343 ms

分步演示

JPA 一级缓存
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。Hibernate ORM 6.6.4.Final、PostgreSQL JDBC 42.7.4、本机 PostgreSQL 16.15(默认 READ COMMITTED);另一个并发事务用 JDBC 直接提交;堆内存取 System.gc() 之后的 used,-Xmx1g
    • 没有验证:二级缓存(hibernate.cache.use_second_level_cache)与查询缓存、@Version 乐观锁(下一篇)、REPEATABLE READ / SERIALIZABLE 隔离级别下的同一场景、@DynamicUpdate、Spring 的 OpenEntityManagerInView 对缓存生命周期的影响
    京ICP备2026067502号-1京公网安备11010502063487号