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 走",失效问题就都能解释。

笔记加载中…