← 返回题目列表

MyBatis 一级缓存和二级缓存有什么区别?

高频 中等 第 9 / 24 题 更新于 2026/07/26
MyBatis一级缓存二级缓存

简化版

一级缓存SqlSession 级别,默认开启,同一个 session 内相同查询直接走缓存。二级缓存:Mapper 命名空间(namespace)级别,可跨 session 共享,但默认关闭、需手动开启。作用范围一个比一个大,一致性风险也一个比一个高。

详细版

一级缓存(本地缓存)

  • 作用域是单个 SqlSession,默认开启,无法关闭(可设置为 STATEMENT 级别弱化);
  • 同一个 session 里,用相同的 statement + 相同参数查同一个东西,第二次直接返回缓存结果,不查库;
  • 失效时机:执行了 insert/update/delete、手动 clearCache()commit/rollback、或 session 关闭,缓存被清空。

二级缓存

  • 作用域是 Mapper 的 namespace,多个 SqlSession 共享;
  • 默认不开,要在配置里开 cacheEnabled 并在对应 Mapper 加 <cache/>
  • 生命周期更长、共享范围更大,因此数据一致性问题更突出:多个入口改数据、或分布式多节点部署时,本地二级缓存很容易读到脏数据。

完整版教学

一、两级缓存的定位差异

抓住区别:一级缓存服务「一次会话内的重复查询」,二级缓存服务「跨会话的重复查询」

  • 一级缓存像「方法内的局部变量」——范围小、生命周期短(一个 session 通常对应一次请求/一个事务),风险低,所以默认开着也没大问题。
  • 二级缓存像「跨请求的全局缓存」——范围大、活得久,一旦数据被别的途径改了而缓存没更新,就读到旧数据,所以设计者让它默认关闭、交给你谨慎决定。
维度一级缓存二级缓存
作用域单个 SqlSessionMapper namespace
默认状态默认开启默认关闭,需要配置
共享范围不跨 session可跨 session,同 JVM 内共享
写入时机查询后放入本地缓存session 提交或关闭后才进入二级缓存
主要风险长 session 读到旧值多入口修改、跨节点不一致、namespace 割裂

记忆钩子:一级缓存是“当前会话的小便签”,二级缓存是“Mapper 命名空间的公共公告板”;范围越大,失效越难。

二、一级缓存的一个经典坑

一级缓存虽然默认开、看着省心,但在长事务 + 并发下会出问题:

// 同一个 SqlSession 内
User u1 = mapper.selectById(1);   // 查库,结果进一级缓存
// ……此时别的事务把 id=1 的记录改了并提交……
User u2 = mapper.selectById(1);   // 直接返回缓存里的旧 u1,没查库!

因为一级缓存不知道「外部有人改了数据」,同一个 session 里第二次查还是旧值。所以对实时性要求高的场景,要么缩短 session 生命周期,要么把一级缓存设为 STATEMENT 级别(每条语句后清)。

T1 SqlSession-A: selectById(1) -> 数据库 age=20,放入一级缓存
T2 SqlSession-B: update age=21 并 commit
T1 SqlSession-A: 再次 selectById(1) -> 命中一级缓存,仍返回 age=20

如果一个 Web 请求只持有很短的 SqlSession,这个风险通常可控;如果把 SqlSession 跨多个业务步骤长期持有,就容易把“本地缓存”误用成“真实最新数据”。

三、二级缓存为什么在生产中常被禁用

二级缓存看着很美(跨 session 复用、减少查库),但生产环境很多团队直接关掉它,原因是它的一致性坑太多:

  1. 多入口修改:只要有人不经过 MyBatis(如直接改库、别的服务改、定时任务改)改了数据,二级缓存完全感知不到,一直返回旧值。
  2. 分布式失效:二级缓存是每个 JVM 节点本地的。集群部署时,节点 A 改了数据清了自己的缓存,节点 B 的缓存还是旧的——脏读。
  3. namespace 割裂:二级缓存按 namespace 隔离,如果多个 Mapper 操作同一张表,一个 Mapper 的更新不会清另一个 Mapper 的缓存。

记忆点:需要跨请求缓存,生产上更推荐用 Redis 这类外部分布式缓存自己控制,而不是依赖 MyBatis 本地二级缓存——外部缓存能统一失效、跨节点共享,一致性可控得多。

四、查询走缓存的完整顺序

MyBatis 查询时的查找顺序是:二级缓存 → 一级缓存 → 数据库。先看 namespace 的二级缓存有没有,没有再看当前 session 的一级缓存,都没有才查库,查完回填缓存。

(注意一个细节:二级缓存要求查询结果所在的 session 提交或关闭后才真正写入二级缓存,因为只有事务提交了,数据才「确定有效」,此前只是暂存。)

五、缓存键和失效要一起理解

缓存不是只按 SQL 文本粗暴保存。MyBatis 会结合 statement id、分页信息、SQL、参数等生成缓存键;同一条 Mapper 方法,不同参数对应不同缓存项。增删改默认会清理当前 namespace 相关缓存,查询也可以通过 useCacheflushCache 控制缓存行为。

一个数字场景:selectById(1)selectById(2) 是同一 statement,但参数不同,会形成两个缓存项;selectById(1) 第一次查库,第二次同 session 命中一级缓存。如果执行一次 updateUser,默认会清空本地缓存,避免同 session 后续读到刚被自己改掉的旧值。

二级缓存的难点在于“谁来清”。同一 namespace 内的更新容易触发清理;但如果另一个 Mapper、另一个服务、数据库脚本修改了同一张表,本 namespace 的二级缓存未必知道。所以面试时不要只说“二级缓存能跨 session”,还要主动补上“一致性边界很难控制”。

再看集群场景:两台应用节点 A、B 都开启本地二级缓存。A 节点通过 UserMapper 更新用户并清了 A JVM 内的 namespace 缓存,B 节点内存里的旧缓存不会天然收到通知。除非引入外部统一缓存、消息失效或干脆关闭二级缓存,否则用户请求打到 B 时仍可能读旧值。

这也是为什么很多生产项目只保留一级缓存,把跨请求缓存放到 Redis 或 Caffeine + 统一失效策略里管理。MyBatis 缓存贴近 SQL 层,方便但信息有限;业务缓存贴近业务语义,更容易按用户、租户、权限和数据变更路径设计失效。

还有一个容易忽略的点:缓存命中不代表“业务上可复用”。同一条 SQL 如果缺少租户、权限、软删除等条件,缓存只会忠实保存错误查询结果;所以缓存设计必须建立在 SQL 条件正确的前提上,不能用缓存掩盖查询边界问题。

六、常见误区与追问

  • 误区:二级缓存默认开启。 二级缓存默认关闭,需要全局 cacheEnabled 和 Mapper <cache/> 等配置配合。
  • 误区:一级缓存永远不会读到旧数据。 长 SqlSession 中,外部事务提交的修改不会自动通知当前 session 的一级缓存。
  • 追问:为什么二级缓存生产中常被禁用? 多入口修改、分布式多节点和 namespace 割裂都会造成失效困难,读到旧数据的风险较高。
  • 追问:二级缓存什么时候真正写入? 通常要等 SqlSession 提交或关闭后,查询结果才会进入二级缓存,避免未提交数据被跨 session 看到。
  • 误区:MyBatis 二级缓存可以直接替代 Redis。 它主要是本地 namespace 缓存,跨节点一致性和统一失效能力远弱于外部分布式缓存。
  • 追问:增删改为什么会清缓存? 更新会改变查询结果,默认清理相关本地缓存和 namespace 缓存,避免后续查询读旧值。

七、加强记忆

一级缓存是 SqlSession 级、默认开、增删改/提交即失效;二级缓存是 namespace 级、跨 session 共享、默认关。二级缓存因多入口修改和分布式本地缓存不一致,生产中常被禁用,真要跨请求缓存优先用 Redis 自己管控一致性。