MyBatis 一级缓存和二级缓存有什么区别?
简化版
一级缓存:SqlSession 级别,默认开启,同一个 session 内相同查询直接走缓存。二级缓存:Mapper 命名空间(namespace)级别,可跨 session 共享,但默认关闭、需手动开启。作用范围一个比一个大,一致性风险也一个比一个高。
详细版
一级缓存(本地缓存):
- 作用域是单个
SqlSession,默认开启,无法关闭(可设置为 STATEMENT 级别弱化); - 同一个 session 里,用相同的 statement + 相同参数查同一个东西,第二次直接返回缓存结果,不查库;
- 失效时机:执行了 insert/update/delete、手动
clearCache()、commit/rollback、或 session 关闭,缓存被清空。
二级缓存:
- 作用域是 Mapper 的 namespace,多个
SqlSession共享; - 默认不开,要在配置里开
cacheEnabled并在对应 Mapper 加<cache/>; - 生命周期更长、共享范围更大,因此数据一致性问题更突出:多个入口改数据、或分布式多节点部署时,本地二级缓存很容易读到脏数据。
完整版教学
一、两级缓存的定位差异
抓住区别:一级缓存服务「一次会话内的重复查询」,二级缓存服务「跨会话的重复查询」。
- 一级缓存像「方法内的局部变量」——范围小、生命周期短(一个 session 通常对应一次请求/一个事务),风险低,所以默认开着也没大问题。
- 二级缓存像「跨请求的全局缓存」——范围大、活得久,一旦数据被别的途径改了而缓存没更新,就读到旧数据,所以设计者让它默认关闭、交给你谨慎决定。
| 维度 | 一级缓存 | 二级缓存 |
|---|---|---|
| 作用域 | 单个 SqlSession | Mapper 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 复用、减少查库),但生产环境很多团队直接关掉它,原因是它的一致性坑太多:
- 多入口修改:只要有人不经过 MyBatis(如直接改库、别的服务改、定时任务改)改了数据,二级缓存完全感知不到,一直返回旧值。
- 分布式失效:二级缓存是每个 JVM 节点本地的。集群部署时,节点 A 改了数据清了自己的缓存,节点 B 的缓存还是旧的——脏读。
- namespace 割裂:二级缓存按 namespace 隔离,如果多个 Mapper 操作同一张表,一个 Mapper 的更新不会清另一个 Mapper 的缓存。
记忆点:需要跨请求缓存,生产上更推荐用 Redis 这类外部分布式缓存自己控制,而不是依赖 MyBatis 本地二级缓存——外部缓存能统一失效、跨节点共享,一致性可控得多。
四、查询走缓存的完整顺序
MyBatis 查询时的查找顺序是:二级缓存 → 一级缓存 → 数据库。先看 namespace 的二级缓存有没有,没有再看当前 session 的一级缓存,都没有才查库,查完回填缓存。
(注意一个细节:二级缓存要求查询结果所在的 session 提交或关闭后才真正写入二级缓存,因为只有事务提交了,数据才「确定有效」,此前只是暂存。)
五、缓存键和失效要一起理解
缓存不是只按 SQL 文本粗暴保存。MyBatis 会结合 statement id、分页信息、SQL、参数等生成缓存键;同一条 Mapper 方法,不同参数对应不同缓存项。增删改默认会清理当前 namespace 相关缓存,查询也可以通过 useCache、flushCache 控制缓存行为。
一个数字场景: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 自己管控一致性。