MyBatis 一级缓存
MyBatis 有两级缓存:一级缓存是每个 SqlSession 自带的本地缓存,默认开启且无法关闭(只能调作用域);二级缓存是跨 SqlSession 的命名空间级缓存(下一章)。理解一级缓存的存活范围,是避免"缓存没生效/数据没更新"类困惑的关键。
一级缓存的存放位置
一级缓存属于某个 SqlSession 实例,本质是会话内部的一个 Map:
SqlSession A ──→ 自己的本地缓存(存 key = 语句ID+参数+环境…)
SqlSession B ──→ 自己的本地缓存(彼此独立、互不可见)
默认 localCacheScope=SESSION:同一 SqlSession 内,相同语句 + 相同参数的查询直接返回缓存结果,不再访问数据库。
命中演示
try (SqlSession session = factory.openSession(true)) {
User u1 = session.selectOne("...UserMapper.selectById", 1);
User u2 = session.selectOne("...UserMapper.selectById", 1);
System.out.println(u1 == u2); // 输出:true(第二次直接命中缓存,是同一个对象)
}
打开日志会看到:第一次查询打印 SQL,第二次没有任何 SQL——数据来自缓存。
注意:返回的是同一个对象
一级缓存保存的是对象引用,两次查询拿到的是同一个实例。这带来一个隐患:
User u1 = session.selectOne("...selectById", 1);
u1.setName("被改了"); // 缓存里的对象也被改
User u2 = session.selectOne("...selectById", 1);
System.out.println(u2.getName()); // 输出:被改了(不是数据库里的原值!)
所以在会话中修改查询结果对象要格外小心,必要的话拷贝后再改。
缓存何时失效(重点)
遇到"为什么又查库了"的问题,逐个对照下面:
| 场景 | 结果 |
|---|---|
| 换了另一个 SqlSession | 不共享缓存,重新查库 |
| 查询参数 / 语句不同 | 缓存 key 不同,重新查库 |
| 同一会话执行了 insert/update/delete | 缓存被整体清空 |
| commit() / rollback() / close() | 缓存被清空 |
| 手动调用 session.clearCache() | 缓存被清空 |
| localCacheScope 设为 STATEMENT | 每条语句结束即清,形同关闭 |
增删改会清空缓存,是为了避免读到"旧数据":比如先查了 id=1,又 update 了它,再查就必须拿到新值。
<settings>
<!-- 每条语句执行完就清空一级缓存 -->
<setting name="localCacheScope" value="STATEMENT"/>
</settings>
与 Spring 集成后的大坑
原生写法里 SqlSession 生命周期由你掌控,一级缓存还算直观。一旦接入 Spring(第 15 章):
- 没有事务时:每次 Mapper 方法调用都会新建并关闭一个 SqlSession,一级缓存"查完即弃",等于没缓存。
- 有事务时:一个事务共用一个 SqlSession,事务内同参数重复查询才可能命中一级缓存。
所以经常有人说"MyBatis 一级缓存没什么用",多半是在 Spring 无事务场景下的体感。
小结
一级缓存 = 每个 SqlSession 内部的本地缓存,默认开启;同会话同参数重复查询才命中,执行增删改或提交/关闭/clearCache 即失效。记住"缓存跟着 SqlSession 走",失效问题就都能解释。