ORM 缓存抓取

共 20 题
📑 题目列表 20 题
#
★★★

1. MyBatis 一级缓存与二级缓存的实现差异?

MyBatis 的一级缓存与二级缓存分别是什么?它们在实现上有何差异?

  • 一级缓存的作用域(SqlSession)
  • 二级缓存的作用域(namespace)
  • 缓存失效与并发控制差异

MyBatis 一级缓存默认开启,作用域是 SqlSession(一个数据库会话),在同一个 SqlSession 内对相同 SQL 的查询结果会被缓存,避免重复查询;但 SqlSession 关闭或执行更新操作后缓存失效。二级缓存需要显式开启(cache 标签),作用域是 mapper 的 namespace(跨 SqlSession 共享),由多个 SqlSession 共享,需要配置 eviction(淘汰策略)、flushInterval(刷新间隔)、size、readOnly 等参数。一级缓存是 Session 级别、默认开启、无需配置;二级缓存是 namespace 级别、需显式配置、跨会话共享,存在并发与脏读风险。

一级缓存作用域小、天然安全(会话内一致);二级缓存作用域大、跨会话,需要处理并发一致性、多线程安全与序列化,因此风险更高。两级缓存默认都只能缓存查询结果,更新操作会使其失效。

#
★★★

2. N+1 问题的检测与优化,JOIN FETCH、@EntityGraph、子查询?

什么是 N+1 查询问题?如何检测与优化?JOIN FETCH、@EntityGraph、子查询分别如何使用?

  • N+1 问题的定义与成因
  • 检测手段(SQL 日志、Hibernate 统计)
  • 优化手段(JOIN FETCH、@EntityGraph、子查询)

N+1 问题指:先查询出 N 条主数据,再为每条主数据逐个查询其关联数据,共执行 1 次主查询 + N 次关联查询,性能极差。检测可通过开启 SQL 日志/统计观察到大量重复 SQL,或使用 Hibernate 的 show_sql 与统计输出。优化手段:JOIN FETCH(HQL/JPQL 中一次性 join 抓取关联集合)、@EntityGraph(JPA 定义的抓取图,定义关联的抓取路径)、子查询(将关联查询放入一个子查询统一取回)。三者都旨在减少查询次数,把 N+1 合并为 1 次或少量查询。

N+1 的根源是默认懒加载(LAZY)导致关联对象逐个加载。JOIN FETCH 与 @EntityGraph 是把关联一次性拉回,但要注意集合 JOIN 可能产生笛卡尔积暴涨;子查询则避免 JOIN 放大行数。

-- 用 JOIN 一次取出主数据及其关联,避免 N+1
SELECT o.*, i.* FROM orders o JOIN order_items i ON i.order_id = o.id WHERE o.id = :id
#
★★★

3. ORM(Hibernate、MyBatis、JPA、SQLAlchemy)的 N+1 查询问题?

ORM(Hibernate、MyBatis、JPA、SQLAlchemy)中的 N+1 查询问题是什么?如何产生与避免?

  • 各 ORM 中 N+1 的成因
  • 关联加载策略
  • 通用优化手段

N+1 问题在各类 ORM 中普遍存在:当主实体关联了子实体集合,且关联采用懒加载时,访问每个主实体的子集合都会触发一次额外查询,形成 1+N 次查询。Hibernate/JPA 通过 fetch join、@EntityGraph、@BatchSize 避免;MyBatis 通过 resultMap 的嵌套查询(会导致 N+1)改用 join 查询或 collection 的 select 谓词优化;SQLAlchemy 通过 eager loading(joinedload、selectinload)避免。本质问题都是"关联对象逐个加载",优化方向是"一次性批量加载"。

无论哪种 ORM,N+1 的根源都是懒加载 + 循环访问单个关联。优化思路统一:用 join 或批量预加载(batch)把关联一次取回,或改用子查询/selectin 减少往返。

#
★★★

4. Hibernate 一级缓存(Session 级)与二级缓存(SessionFactory 级)的生命周期、失效与并发控制?

Hibernate 一级缓存(Session 级)与二级缓存(SessionFactory 级)的生命周期是什么?如何失效?如何做并发控制?

  • 一级缓存生命周期(Session 生命周期)
  • 二级缓存生命周期(SessionFactory 生命周期)
  • 失效机制与并发策略

Hibernate 一级缓存是 Session 级(持久化上下文),生命周期与 Session 相同,Session 打开则存在、关闭则销毁,保存实体实例与加载结果,默认开启且不可跨 Session 共享。二级缓存是 SessionFactory 级,生命周期与应用(SessionFactory)一致,跨 Session 共享,需要显式配置与第三方缓存集成(如 Ehcache、Redis)。失效机制:一级缓存随 Session 关闭或 clear/evict 清空;二级缓存通过缓存 region 的失效(更新时同步失效)、flushInterval 过期、evict 等控制。并发控制:二级缓存通过并发访问策略(read-only/read-write/nonstrict-read-write/transactional)保证多线程访问的一致性。

一级缓存"随 Session 生灭",天然与事务绑定、一致;二级缓存"随应用生灭",跨会话共享,必须靠并发策略与失效机制保证一致性,否则会出现脏读。

#
★★★

5. Hibernate 二级缓存的并发访问策略(read-only/read-write/nonstrict-read-write/transactional)与失效语义

Hibernate 二级缓存的并发访问策略有哪些(read-only/read-write/nonstrict-read-write/transactional)?各自的失效语义是什么?

  • 四种并发策略的含义
  • 各自适用场景
  • 失效与一致性语义

Hibernate 二级缓存的四种并发策略:read-only(只读,适合不变的配置数据,性能最高,不可更新);read-write(读写,通过锁/时间戳保证缓存与数据库一致,适合频繁读写但并发不高的数据);nonstrict-read-write(非严格读写,不保证绝对一致,更新时使缓存失效,下一次查询重新加载,适合读多写少、容忍短暂不一致的数据);transactional(事务性,配合 JTA 等事务性缓存提供器,保证强一致,成本最高)。失效语义:read-write 更新时同步更新缓存并加锁;nonstrict-read-write 更新时仅使缓存条目失效,不主动更新;transactional 依赖底层事务性缓存保证原子性。

策略选择本质是"一致性与性能的权衡"。越往上一致性强、成本高;越往下性能好、一致性弱。大多数业务用 read-write 或 nonstrict-read-write,配置类静态数据用 read-only。

#
★★★

6. Hibernate 的自动 flush(FlushMode.AUTO)会在查询前把脏数据写入数据库,为什么会导致意外写库与性能抖动?如何显式控制 flush 时机?

Hibernate 的自动 flush(FlushMode.AUTO)会在查询前把脏数据写入数据库,为什么会导致意外写库与性能抖动?如何显式控制 flush 时机?

  • FlushMode.AUTO 的 flush 时机
  • 意外写库与性能抖动的原因
  • 显式控制 flush(FlushMode.COMMIT/MANUAL、flush())

Hibernate 默认 FlushMode.AUTO 会在执行查询前自动 flush(把持久化上下文中的脏数据同步到数据库),保证查询结果包含未提交的修改。这会导致两个问题:一是意外写库——本想在事务末尾才提交的变更可能在查询时被提前写入数据库,产生与预期不符的写操作;二是性能抖动——查询前的 flush 会额外执行多条 UPDATE,尤其在有大量脏数据时,导致查询延迟骤增。显式控制方式:设置 FlushMode.COMMIT(仅在提交时 flush)或 FlushMode.MANUAL(完全手动),并在需要时调用 session.flush() 主动同步。

FlushMode.AUTO 的"查询前 flush"保证了会话内一致性,但代价是隐藏的写库与性能开销。对只读查询或读多写少的场景,改为 COMMIT/MANUAL 可显著减少意外写库与抖动。

#
★★

7. 抓取策略(Fetch Strategy),Lazy、Eager、Join Fetch?

抓取策略(Fetch Strategy)中的 Lazy、Eager、Join Fetch 分别是什么?如何选择?

  • Lazy 与 Eager 的区别
  • Join Fetch 的用法
  • 选择依据

Lazy(懒加载)是默认策略,关联对象在使用时才加载,节省资源但可能产生 N+1 与 LazyInitializationException;Eager(立即加载)是关联对象随主实体一起立即加载,简单但可能加载过多不必要数据;Join Fetch 是在查询语句(JPQL/HQL)中用 join fetch 显式指定一次 join 抓取关联,是解决 N+1 的推荐做法。选择上:默认用 Lazy 避免过度加载,需要关联数据时用 JOIN FETCH 或 @EntityGraph 显式批量抓取,避免用 Eager 一次性加载庞大集合。

Lazy 与 Eager 是映射级策略,Join Fetch 是查询级策略。推荐组合:映射用 Lazy,查询时按需用 Join Fetch/@EntityGraph,既避免过度加载又避免 N+1。

#
★★

8. ORM 与原生 SQL 的取舍,复杂查询如何突破 ORM 限制?

ORM 与原生 SQL 如何取舍?复杂查询如何突破 ORM 的限制?

  • ORM 的适用场景与局限
  • 原生 SQL 的适用场景
  • 复杂查询的突破方式

ORM 适合标准的 CRUD、对象关系映射、跨表简单关联,开发效率高、类型安全;但面对复杂查询(多表 JOIN、子查询、窗口函数、动态聚合、DB 特有功能)时,ORM 生成的 SQL 往往难以优化、难读、难调优。突破方式:在 ORM 中使用原生 SQL(写 SQL 或 JPA 的 native query、MyBatis 的 XML SQL)、使用数据库视图降低 ORM 侧复杂度、或用 MyBatis 这类对 SQL 更友好的框架。取舍原则:简单 CRUD 用 ORM,复杂查询用原生 SQL/视图,避免 ORM 生成低效 SQL 又难以排查。

ORM 的价值在映射与开发效率,原生 SQL 的价值在可控与可优化。最佳实践是"ORM 管日常、原生 SQL 管复杂",两者结合使用。

#
★★

9. 事务隔离级别设置(在连接池 ORM 与批处理范畴内)?

在连接池、ORM 与批处理范畴内,事务隔离级别如何设置?

  • 隔离级别设置的位置(连接/事务/ORM)
  • 事务隔离级别与连接池的关系
  • 各级别语义

事务隔离级别可在多个层面设置:JDBC 通过 Connection.setTransactionIsolation() 在连接上设置;Spring 通过 @Transactional(isolation=...) 在方法上设置;ORM 通过 Hibernate 的 hibernate.connection.isolation。由于连接池复用连接,连接上的隔离级别会残留,需在取出连接时重置或使用 Spring 的 propagate 机制。隔离级别包括读未提交、读已提交、可重复读、串行化,分别控制脏读、不可重复读、幻读。在连接池下,应保证每个业务事务显式设置隔离级别,避免复用连接残留的高级别(如可重复读、串行化)导致性能下降。

连接池场景下隔离级别是"连接级状态",会被复用,因此在事务开始处显式设置、结束后恢复,避免对后续请求造成影响,是重要实践。

#
★★

10. MyBatis 一级缓存失效场景,跨 SqlSession、事务提交、SQL 变更,以及二级缓存跨会话的脏读风险?

MyBatis 一级缓存的失效场景有哪些?二级缓存跨会话的脏读风险是什么?

  • 一级缓存失效场景(跨 SqlSession、事务提交、SQL 变更)
  • 二级缓存跨会话的脏读风险
  • 缓存一致性

MyBatis 一级缓存失效场景包括:跨 SqlSession(不同会话缓存不共享)、事务提交/回滚(更新操作触发缓存清空)、SQL 变更(不同 SQL 或带参数的 SQL 不命中)、执行增删改操作后缓存失效。二级缓存是跨 SqlSession 共享的,脏读风险在于:多个会话共享缓存,若一个会话更新了数据但缓存未及时失效,其他会话会读到旧数据;事务未提交时缓存可能已更新,导致其他会话读到未提交数据;不同 namespace 的关联更新也需通过 cache-ref 关联失效。因此二级缓存需谨慎配置,避免敏感数据与高频更新数据。

一级缓存失效由"会话结束/更新操作"触发,安全;二级缓存跨会话共享,一致性依赖"更新时失效"机制,机制不完善就会脏读。

#
★★

11. JPA 的抓取策略,fetch=FetchType.LAZY/EAGER、@BatchSize、@EntityGraph 在避免 N+1 时的选择?

JPA 的 fetch=FetchType.LAZY/EAGER、@BatchSize、@EntityGraph 在避免 N+1 时如何选择?

  • FetchType.LAZY/EAGER 的语义
  • @BatchSize 批量加载
  • @EntityGraph 抓取图

JPA 中 FetchType.LAZY 表示懒加载,EAGER 表示立即加载。避免 N+1 时:@EntityGraph 是推荐方式,通过 attributeNodes 定义关联的抓取路径,让查询一次性 join 抓取关联,避免逐个加载;@BatchSize 用于批量懒加载,当访问多个实体的懒集合时,按批次一次性加载多个集合,减少查询次数(如 fetch 10 个实体只发 1 条 IN 查询)。选择上:优先用 @EntityGraph 做查询级抓取控制,@BatchSize 适合对懒加载做批量兜底,避免滥用 EAGER 导致每次查询都加载庞大关联。

@EntityGraph 精确控制"哪次查询抓哪些关联",@BatchSize 是对懒加载的批量优化,两者都从"减少查询次数"角度解决 N+1,比全局 EAGER 更可控。

#
★★

12. Hibernate 查询缓存(Query Cache)的失效机制与使用风险

Hibernate 查询缓存(Query Cache)的失效机制是什么?使用风险有哪些?

  • 查询缓存的机制(缓存查询结果及标识符)
  • 失效机制(依赖相关表与查询空间)
  • 使用风险(脏读、失效不精确)

Hibernate 查询缓存用于缓存查询语句的结果(缓存的是标识符列表,再结合二级缓存取实体),需配合二级缓存使用。失效机制基于"查询空间"(query space):被查询涉及的表所在 region 被更新时,相关查询缓存条目失效。风险包括:查询缓存失效粒度粗(可能过度失效,导致命中率低);若底层数据被其他途径(原生 SQL、其他应用)修改而未被 Hibernate 感知,缓存不会失效,产生脏读;缓存实体标识符而不缓存实体,需要二级缓存配合,否则仍会访问数据库。因此查询缓存适合低频变更、高频查询的数据,并需小心失效一致性。

查询缓存的价值在于复用查询结果,但其失效依赖"对表更新的感知",一旦有绕过 Hibernate 的写操作就会脏读。这是它比二级缓存更危险的原因。

#
★★

13. ORM 的一级缓存(Session/EntityManager)与事务边界,为什么跨事务复用持久化上下文会导致脏读与连接泄漏?如何用 clear/evict 或短生命周期 Session 规避?

ORM 一级缓存(Session/EntityManager)与事务边界的关系是什么?为什么跨事务复用持久化上下文会导致脏读与连接泄漏?如何规避?

  • 一级缓存与事务边界的绑定
  • 跨事务复用持久化上下文的脏读与连接泄漏问题
  • 规避方式(clear/evict、短生命周期 Session)

一级缓存(持久化上下文)应当与事务边界对齐:一个事务对应一个持久化上下文,事务结束(提交/回滚)时上下文也应关闭。若跨事务复用持久化上下文,会出现:一是脏读——上个事务加载的实体仍被缓存,下个事务读到旧数据;二是连接泄漏——持久化上下文持有数据库连接,跨事务复用导致连接在事务结束后仍被占用,无法归还连接池,最终连接耗尽。规避方式:使用短生命周期 Session/EntityManager(每次业务操作新建、结束即关闭),或在需要时调用 clear()/evict() 清空缓存,或结束后关闭会话归还连接。

持久化上下文不仅缓存数据,还持有连接/事务资源。跨事务复用破坏"事务与会话绑定"的约定,导致一致性(脏读)与资源(连接)双重问题。Spring 的 OpenSessionInView 就是这样一种需要谨慎使用的模式。

#
★★

14. 为什么一级缓存只对按 ID 加载(get/load)命中,而 HQL/JPQL 查询结果不会自动放入一级缓存?查询缓存与一级缓存的配合规则是什么?

为什么一级缓存只对按 ID 加载(get/load)命中,而 HQL/JPQL 查询结果不会自动放入一级缓存?查询缓存与一级缓存的配合规则是什么?

  • 一级缓存按 ID 命中(identity map)
  • HQL/JPQL 查询结果不自动入一级缓存
  • 查询缓存与一级缓存的配合

Hibernate 一级缓存本质是"身份映射表"(identity map),以实体类型+ID 为 key 缓存实体实例,因此 get/load 按 ID 加载会命中。而 HQL/JPQL 查询执行的是 SQL 查询,查询结果是一组标识符,Hibernate 不会把这些查询结果自动放入一级缓存(因为不知道查询变化与失效时机),而是每次真正执行 SQL。若想缓存查询结果,需启用查询缓存(Query Cache),它缓存查询返回的标识符列表,再结合一级/二级缓存取实体。配合规则是:查询缓存缓存"标识符列表",实体本身靠一级/二级缓存;查询缓存需依赖二级缓存,且基于查询空间失效。

一级缓存按标识符(ID)缓存实体,查询缓存缓存"查询逻辑的标识符结果"。两者配合:查询缓存命中后仍需从二级缓存取实体,若二级缓存未命中则访问数据库。

#
★★

15. MyBatis 二级缓存的 cache 标签参数(flushInterval、size、readOnly、eviction)如何影响并发读写与过期行为?

MyBatis 二级缓存的 cache 标签参数(flushInterval、size、readOnly、eviction)如何影响并发读写与过期行为?

  • flushInterval 刷新间隔
  • size 缓存容量
  • readOnly 只读

MyBatis 二级缓存 cache 标签参数:flushInterval 是刷新间隔(毫秒),超过后缓存条目过期,控制缓存有效时长;size 是缓存最大可用对象数,超过后按淘汰策略淘汰;readOnly 为 true 时缓存只读,返回同一对象引用,性能高但不允许修改,为 false 时返回序列化副本,安全性高但性能低;eviction 是淘汰策略(LRU、FIFO、SOFT、WEAK),决定缓存满时淘汰哪些条目。这些参数共同影响:flushInterval 决定过期速度,size+eviction 决定容量与淘汰,readOnly 决定并发安全与性能。

readOnly 影响"是否允许共享对象引用",readOnly=true 时多线程共享同一对象,若被修改会互相污染,因此只适合不可变数据;eviction 与 size 决定缓存空间管理,防止缓存无限增长。

#

16. Hibernate 批量抓取(@BatchSize)与二级缓存组合在避免 N+1 时的注意点?

Hibernate 批量抓取(@BatchSize)与二级缓存组合在避免 N+1 时有什么注意点?

  • @BatchSize 的批量加载
  • 与二级缓存的组合
  • 注意点

@BatchSize 让懒加载关联按批次批量加载(如一次 IN 查询取多个实体),减少查询次数。与二级缓存组合时注意:二级缓存命中可避免数据库访问,但批量抓取仍会发查询;若二级缓存未命中,@BatchSize 会一次性批量加载多个实体的关联,需注意 SQL 的 IN 参数数量与数据库性能;同时缓存的数据若是可变对象,需与 read-only 策略配合,避免缓存与数据库不一致。总体注意"批量抓取减少查询次数"与"二级缓存减少数据库访问"是两个独立维度,组合时需兼顾缓存一致性(失效机制)与批量 SQL 的负载。

组合使用的价值是"双重减少":二级缓存减少 DB 访问,@BatchSize 减少懒加载的查询次数。但要注意缓存一致性(更新失效)与批量加载的 SQL 复杂度。

#

17. ORM 缓存的分布式一致性,本地缓存与 Redis 的取舍?

ORM 缓存的分布式一致性如何处理?本地缓存与 Redis 怎么取舍?

  • 本地缓存 vs 分布式缓存
  • 分布式一致性的挑战
  • 取舍依据

本地缓存(如 Hibernate 二级缓存存于 JVM 内)性能最高但每个应用实例各自一份,多个实例间数据不一致,需要缓存失效广播(如 Redis pub/sub、消息)来同步。Redis 等分布式缓存天然跨实例共享,一致性更好,但增加网络往返,性能略低于本地缓存。取舍:单实例可用本地缓存;多实例且对一致性要求高时用 Redis;对一致性要求极高(金融)则尽量不用缓存或结合失效广播。管理分布式一致性需考虑缓存失效的广播、本地缓存与 DB 的同步时机、以及缓存与 DB 的最终一致。

本质是"性能 vs 一致性"的权衡。本地缓存快但需失效广播,Redis 一致但多一跳。最佳实践常是"本地缓存 + Redis 兜底 + 失效广播"。

#

18. ORM 一级缓存的逐出(evict/clear)与刷新(refresh),什么场景必须绕过一级缓存直查数据库?

ORM 一级缓存的逐出(evict/clear)与刷新(refresh)是什么?什么场景必须绕过一级缓存直查数据库?

  • evict/clear 逐出缓存
  • refresh 刷新缓存
  • 必须绕过一级缓存的场景

evict 将单个实体从一级缓存中移除,clear 清空整个一级缓存,refresh 强制重新从数据库加载覆盖缓存中的实体。当一级缓存中的实体与数据库不一致(例如其他并发事务/其他应用修改了数据、或本会话测试代码注入的数据)时,必须绕过一级缓存直查数据库:一是调用 refresh 强制刷新;二是通过 clear/evict 后重新查询;三是用 bypass 缓存的查询(如 Hibernate 的 setCacheMode)。典型场景:被别人修改过的数据、调试/测试需要读最新值、以及使用原生 SQL 更新的数据。

一级缓存是会话内快照,会掩盖数据库的最新变化。refresh 或 evict 是"打破快照、回到数据库"的手段。

#

19. ORM 缓存与数据库隔离级别的关系(缓存读取是否绕过事务快照)

ORM 缓存与数据库隔离级别的关系是什么?缓存读取是否绕过事务快照?

  • 缓存与隔离级别的关系
  • 缓存读取与事务快照
  • 一致性风险

ORM 缓存(一级/二级缓存)读取的是应用内存,不经过数据库,因此数据库的隔离级别(MVCC 快照)对缓存读取不生效。也就是说,缓存读取绕过事务快照——一个事务通过缓存读到的数据可能不是当前事务快照中的版本,而是过期缓存,导致"不可重复读"甚至脏读(在事务隔离级别下本应保证的一致性被缓存破坏)。这意味着缓存与隔离级别是两套机制:隔离级别保证数据库侧的读一致性,缓存则可能打破它。因此需通过缓存失效、刷新机制保证缓存与数据库一致,尤其在需要严格隔离级别语义的场景。

隔离级别是数据库在事务内提供的快照隔离,缓存是应用层的内存副本,两者不互通。缓存命中时读到的不是数据库快照,可能违反隔离级别语义。

#

20. 把 Hibernate 二级缓存接入 Redis 等分布式缓存时,region 的序列化、失效广播与数据库一致性如何保证?

把 Hibernate 二级缓存接入 Redis 等分布式缓存时,region 的序列化、失效广播与数据库一致性如何保证?

  • region 的序列化
  • 失效广播
  • 数据库一致性

将 Hibernate 二级缓存接入 Redis,需要实现 CacheRegionFactory 等接口:region 对应的缓存数据需可序列化(实体需实现 Serializable,或配置 JSON 序列化),region 名与 Redis key 对应;失效广播通过 Redis 的 pub/sub 或更新时的缓存失效,把失效事件广播到所有应用实例,保证各实例本地缓存同步失效;数据库一致性通过"更新数据库后使缓存失效/更新"的顺序保证,避免先写缓存后写库导致不一致。常见做法:先写库,再失效缓存(Cache-Aside),或使用事务性缓存保证原子性。

分布式缓存接入的关键是"序列化(跨实例可读)+ 失效广播(多实例同步)+ 写库与缓存的顺序(一致性)"。Cache-Aside 是主流方案。