MyBatis 的缓存机制

MyBatis 的缓存机制

一句话总结

MyBatis 有两级缓存:

  • 一级缓存(Local Cache)SqlSession 级别,默认开启 ;同一会话内相同查询可复用结果;执行更新、clearCache()commit/rollback 或会话结束会触发清理。
  • 二级缓存(Second Level Cache)Mapper(namespace) 级别,需 全局开启 + Mapper 开启 ;跨 SqlSession 共享。查询结果通常在会话提交/关闭时才写入二级缓存;更新会按 namespace 刷新相关缓存。

结论:一级缓存解决"同一会话内重复查"二级缓存解决"跨会话读多写少",但要关注一致性与线程/分布式问题。


1. 两级缓存全景图

维度 一级缓存 二级缓存
作用域 SqlSession Mapper namespace
默认 开启 关闭(需配置)
共享性 不共享 同 namespace 的多个 SqlSession 共享
写入时机 查询后立刻放入当前会话缓存 通常在 commit/close 后由会话把数据刷入二级缓存
失效时机 更新/清理/提交回滚/会话结束 更新(默认会 flush)、到期、手动清理
典型问题 会话长导致内存占用;误以为跨会话有效 脏读/一致性;对象需可序列化;分布式需要外部缓存

2. 一级缓存(Local Cache)

2.1 核心特点

  • 范围 :同一个 SqlSession
  • 缓存 key :MyBatis 会基于 MappedStatement、SQL、参数、分页、环境等生成 CacheKey
  • 默认开启:无需配置。

2.2 什么时候命中?

同一 SqlSession 中:

  • SQL 相同
  • 参数相同
  • RowBounds 等影响结果的条件相同

则第二次查询直接返回缓存结果。

2.3 什么时候失效/清理?

常见触发:

  • 执行 INSERT/UPDATE/DELETE
  • 手动调用 sqlSession.clearCache()
  • commit() / rollback()(会清理本地缓存,避免事务边界引发脏数据)
  • close()

注意:实际行为与执行器(Executor)实现有关,但面试记住以上规则基本够用。

2.4 关键配置:localCacheScope

mybatis-config.xml

xml 复制代码
<settings>
  <!-- 默认 SESSION:一级缓存跨 statement 生效(同一 SqlSession 内) -->
  <setting name="localCacheScope" value="SESSION"/>
  <!-- 若设为 STATEMENT:每次 statement 执行完就清掉本地缓存(几乎等于关闭一级缓存) -->
  <!-- <setting name="localCacheScope" value="STATEMENT"/> -->
</settings>
  • SESSION(默认):一级缓存效果最明显。
  • STATEMENT:每次查询执行完清理,适合对一致性极敏感、且不希望复用本地结果的场景。

2.5 一级缓存示例(命中与失效)

java 复制代码
try (SqlSession session = sqlSessionFactory.openSession()) {
    UserMapper mapper = session.getMapper(UserMapper.class);

    User u1 = mapper.selectById(1);
    User u2 = mapper.selectById(1); // 命中一级缓存

    mapper.updateName(1, "newName"); // 触发清理(同 namespace 的更新会导致缓存失效)

    User u3 = mapper.selectById(1); // 重新查库
    session.commit();
}

3. 二级缓存(Second Level Cache)

3.1 开启条件(缺一不可)

  1. 全局开启:
xml 复制代码
<settings>
  <setting name="cacheEnabled" value="true"/>
</settings>
  1. Mapper 开启(二选一):
  • 在 Mapper XML 中加 <cache/>
  • 或使用 @CacheNamespace 注解(不常用,面试可提)

示例:

xml 复制代码
<mapper namespace="com.example.UserMapper">
  <cache eviction="LRU" flushInterval="60000" size="1024" readOnly="false"/>

  <select id="selectById" resultType="User" useCache="true">
    SELECT * FROM user WHERE id = #{id}
  </select>
</mapper>

3.2 关键参数(会背更加分)

参数 含义 常见取值
eviction 淘汰策略 LRU(默认)、FIFOSOFTWEAK
flushInterval 自动刷新间隔(毫秒) 不配则不定时刷新
size 缓存条目上限(近似理解) 1024
readOnly 是否只读 true 返回同一实例(快但风险大);false 返回拷贝(更安全)

3.3 二级缓存写入时机(高频误区)

二级缓存并不是"查完立刻共享出去"。通常流程是:

  1. SqlSession 查询:先查一级缓存;未命中再查二级缓存;再未命中查 DB。
  2. 查 DB 得到结果:先放到当前会话一级缓存
  3. 当会话 commit()close():才把本会话中可缓存的数据刷入二级缓存

因此:

  • 如果你查完不提交/不关闭会话,其他 SqlSession 不一定能命中二级缓存。

3.4 二级缓存失效规则

  • 默认情况下,同 namespace 下的 INSERT/UPDATE/DELETE 会触发缓存刷新。
  • 对单条语句可用:
    • flushCache="true":执行后刷新缓存(更新语句默认就是 true)
    • useCache="false":本次查询不使用二级缓存

4. 同时启用时的查询顺序

  1. 先查 一级缓存(当前 SqlSession)
  2. 再查 二级缓存(namespace 级)
  3. 最后查 数据库

5. 一致性与常见问题

5.1 脏读/过期数据

二级缓存适合"读多写少、允许一定延迟"的数据(配置表、字典表、热点但更新少的数据)。

若并发更新频繁:

  • 可能出现缓存返回旧值(取决于刷新策略、事务提交时机以及调用链)
  • 建议:
    • 缩小/关闭二级缓存范围
    • 对关键查询 useCache=false
    • 或把一致性问题交给更专业的缓存体系(Redis + 失效策略/订阅通知等)

5.2 序列化问题

  • 二级缓存跨会话共享,默认实现通常要求缓存对象可被序列化。
  • 实体类建议实现 Serializable(或使用支持对象拷贝/序列化的缓存实现)。

5.3 分布式环境

MyBatis 自带二级缓存多为单机内存缓存

  • 多实例部署时,各实例二级缓存互不感知,会导致数据不一致。
  • 解决:集成分布式缓存(Redis/Ehcache 集群等)或关闭二级缓存。

6. 扩展:自定义/第三方缓存

MyBatis 的二级缓存基于 Cache 接口,默认实现常见有 PerpetualCache + 各种装饰器(如 LRU)。

若接入第三方缓存(以 Redis 为例),Mapper 可配置:

xml 复制代码
<cache type="org.mybatis.caches.redis.RedisCache"/>

7. 最佳实践(直接背)

  1. 默认依赖一级缓存即可:它天然符合一次请求/一次会话内复用。
  2. 二级缓存只用于读多写少、且能接受一定延迟的数据;热点写多的表慎用。
  3. 线上遇到"查到旧数据/偶现不一致",优先排查:
    • 是否启用了二级缓存
    • 会话是否未提交导致缓存未刷入
    • 更新语句是否按 namespace 正常 flush
    • 是否分布式多实例导致缓存不一致
  4. 分布式场景更推荐:业务侧缓存(Redis)+ 明确失效策略,而不是依赖 MyBatis 二级缓存。

8. 高频面试问答

Q1:一级缓存为什么默认开启?

因为它仅在单个 SqlSession 内生效,能减少同会话内重复查询,收益大且一致性风险相对可控。

Q2:二级缓存为什么要提交/关闭才写入?

为了避免把"未提交事务中的数据"共享出去导致更严重的一致性问题;因此通常在事务边界(commit/close)再刷入。

Q3:useCacheflushCache 有什么用?

  • useCache=false:本次查询不走二级缓存。
  • flushCache=true:执行后刷新缓存(更新默认开启)。

Q4:生产上建议开二级缓存吗?

取决于业务:读多写少、低一致性要求、单机或有一致性方案时可考虑;否则更推荐 Redis 等集中式缓存并配合失效策略。

相关推荐
千里马学框架2 小时前
app性能优化:优化布局层次结构
android·面试·性能优化·framework·分屏·布局·小米汽车
Hx_Ma162 小时前
mybatis练习2
java·数据库·mybatis
青衫码上行3 小时前
高频SQL 50题 | 聚合
数据库·sql·mysql·leetcode·面试
We་ct3 小时前
浏览器渲染流程(完整+面试背诵版)
前端·面试·职场和发展·edge·edge浏览器
香芋Yu13 小时前
【大模型面试突击】10_推理部署与优化
面试·职场和发展
八月的冰可乐14 小时前
【无标题】
ai·面试
莫寒清17 小时前
Java 线程池详解
java·面试
小邓睡不饱耶19 小时前
Spring Boot 3 + MyBatis-Plus 高性能持久层开发实战:从入门到调优
spring boot·后端·mybatis
小李独爱秋20 小时前
模拟面试:说一下数据库主从不同步的原因。
运维·服务器·mysql·面试·职场和发展·性能优化