MyBatis二级缓存配置的本质就三步:在Mapper.xml里加
二级缓存配置完整步骤与参数详解
很多同事第一次配MyBatis二级缓存,照着网上的博客抄了三行配置就以为完事了,实际上这玩意儿的完整链路涉及全局开关、Mapper级别声明、POJO序列化,以及缓存策略的取舍。
全局总开关:setting配置
MyBatis的配置文件(mybatis-config.xml)里有一个叫cacheEnabled的全局开关,它默认值是true,严格来说你什么都不用改,但行业共识认为在团队协作项目中,显式声明这个参数能避免新同事误关缓存。
<settings>
<!-- 全局二级缓存总开关,默认true,建议显式声明 -->
<setting name="cacheEnabled" value="true"/>
</settings>
注意,cacheEnabled控制的只是二级缓存的“允许启用”状态,它开了不代表每个Mapper都有缓存,还得看每个Mapper有没有单独声明。
Mapper层面的核心配置
接下来就是网上教程最爱讲、也是踩坑最多的地方,你在每个Mapper.xml文件里加上<cache/>标签,这个Mapper的查询结果就会进入二级缓存。
<mapper namespace="com.example.mapper.UserMapper">
<!-- 开启二级缓存 -->
<cache
eviction="LRU"
flushInterval="60000"
size="512"
readOnly="false"/>
</mapper>
这几个参数是面试高频考点,也是生产调优的关键,逐个解释一下:
- eviction:回收策略,默认
LRU(最近最少使用),还有FIFO(先进先出)、SOFT(软引用)、WEAK(弱引用),绝大多数业务场景用LRU就够了,软引用和弱引用会让缓存行为变得难以预测,不建议碰。 - flushInterval:刷新间隔,单位毫秒,默认不设置就是没有刷新间隔,缓存直到被更新操作触发清空才会失效,设置成60000就是一分钟强制清空一次,适合数据时效性敏感的场景。
- size:最多缓存多少个对象,默认1024,别贪大,对象一多内存压力就上来了。
- readOnly:只读模式。
true时直接返回缓存对象的引用,性能好但不安全;false时返回对象的序列化拷贝,更安全但稍慢。
实体类序列化是硬性要求
如果你的readOnly设置的是false(默认值),那缓存的对象要经过序列化反序列化过程。实体类必须实现Serializable接口

,不实现的话,查询第二次就会直接报NotSerializableException。
public class User implements Serializable {
private static final long serialVersionUID = 1L;
// ... 其他字段
}
这个坑在mybatis二级缓存配置不生效的案例里占了相当一部分比例,很多人配了<cache/>标签,看着第一次查询走缓存,第二次就报错,十有八九是实体类没实现序列化。
与Spring Boot整合时的额外注意点
如果你用的是Spring Boot + mybatis-spring-boot-starter,那我得提醒一句:光在XML里加标签可不够,还要确认@MapperScan扫描的路径没问题,并且事务边界要清晰,二级缓存是namespace级别的,跨Mapper查询默认不会走缓存,这是MyBatis设计上的一个限制。
为什么你的二级缓存不生效
接到过很多类似的咨询:“我按教程配了<cache/>标签,为什么SQL还是每次都执行?”这个问题在百度上搜“mybatis二级缓存不生效”能翻出一堆帖子,但答案基本都集中在下面几个原因里。
缓存作用域与namespace绑定
二级缓存的作用域是单个Mapper的namespace,你在UserMapper.xml里加了缓存,那只有UserMapper里的查询方法能用上;OrderMapper里查User表的数据,不好意思,不共享缓存。
这种设计在实际项目中很容易踩雷,常见的场景:订单列表查询需要关联展示用户信息,你的SQL写在OrderMapper里,即使UserMapper的缓存是热乎的,OrderMapper的查询也绕过缓存直接打数据库。
手工提交事务才生效
这可能是最容易被忽略的点,MyBatis的二级缓存机制要求SQLSession必须提交事务,查询结果才会写入二级缓存,如果你在Spring里没加@Transactional,或者代码里查完没调sqlSession.commit(),那缓存永远写不进去。
// 错误示范:只查询不提交
SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectById(1);
// 没commit,缓存不生效
// 正确示范:查询后提交事务
SqlSession session = sqlSessionFactory.openSession();
try {
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectById(1);
session.commit(); // 结果写入二级缓存
} finally {
session.close();
}
更新操作导致缓存大面积失效
这是二级缓存最鸡肋的地方。只要Mapper里有任意一条INSERT、UPDATE、DELETE语句执行,整个namespace的缓存都会被清空。 换句话说,如果一个表频繁写入,你的缓存命中率会低到令人发指。
这就好比一个储物柜,你刚把东西放进去,旁边有人一锤子砸开把全部东西都拿走了,你再放,他又砸,一来二去,你干脆不放东西了。

分布式环境下基本不可用
这也是为什么很多架构师在技术选型时直接抛弃MyBatis二级缓存,单机部署还行,一旦上了微服务、多实例部署,每台机器各维护一份缓存,数据一致性瞬间爆炸,A实例更新了数据,B实例还在用旧缓存,业务上出了错,你还没法说清楚是谁的锅。
二级缓存与Redis缓存的取舍
现在整个Java圈子对MyBatis二级缓存的态度已经比较务实了,在我这些年接触的项目里,直接用MyBatis二级缓存扛业务缓存的做法已经越来越少,取而代之的是Redis等外部缓存中间件。
性能与一致性对比
如果用一句话概括:MyBatis二级缓存是JVM进程内缓存,Redis是独立的外部缓存服务,这个区别直接决定了两者在性能和一致性上的表现。
我整理过一个对比表格,可以看得更直观:
| 维度 | MyBatis二级缓存 | Redis缓存 |
|---|---|---|
| 存储位置 | 应用服务器JVM内存 | 独立缓存服务 |
| 访问速度 | 最快,零网络开销 | 快,但有网络IO开销 |
| 数据一致性 | 多实例天然不一致 | 集中存储,天然一致 |
| 缓存容量 | 受限于单机内存 | 可横向扩展 |
| 适用场景 | 单机部署、读多写极少 | 分布式、高并发、读写频繁 |
什么时候还能用二级缓存
纯从技术角度说,有些场景下二级缓存还是有其价值的。字典表、配置表这类几乎不更新的数据,用二级缓存简直完美,比如省市区表、支付渠道表、商品分类表,这些数据一个月都不见得改一次,二级缓存的清空策略对它们来说几乎没有影响。
再比如单机部署的内部管理系统,并发量不大,用户量几百人,用二级缓存降低成本,完全没问题。
什么时候必须抛弃它
如果你遇到下面这些场景,建议直接上Redis或者干脆只用一级缓存:
- 服务做了集群部署,且对数据一致性要求较高
- 业务表写入频繁,缓存会被频繁清空
- 单个Mapper的查询结果集过大,容易撑爆堆内存
- 团队对MyBatis缓存机制理解不深,容易误用导致线上故障
我在一些技术社群里看到过一种说法:把二级缓存当成“锦上添花”而非“雪中送炭”,这句话挺形象,核心业务的热点数据,哪怕让Redis多扛一倍请求,也好过在本地缓存里踩数据不一致的坑。
生产环境下的二级缓存调优实践
如果你评估下来,确实要在项目里继续用二级缓存,那下面的实践建议能帮你少走弯路。
合理设置cache参数
flushInterval

的设置原则是比业务可以容忍的脏数据窗口稍短,比如商品库存允许5秒延迟,那flushInterval设4000毫秒左右比较合适。size的设置要估算单条缓存对象的内存,别拍脑袋填个1024就以为万事大吉。
<cache
eviction="LRU"
flushInterval="30000"
size="256"
readOnly="true"/>
这里把readOnly设成true有个前提:你的业务代码不会修改从Mapper取出来的对象,如果查出来之后要改字段再传回去,那必须用false,否则会污染缓存,引发难以排查的线上问题。
缓存数据变更策略
一个很有效的做法是把频繁更新的Mapper和几乎不变的Mapper分开,比如UserMapper频繁更新,那就不给它加二级缓存,让查询走一级缓存或Redis;而RegionMapper这种万年不变的,放心大胆开二级缓存。
还有一点提醒一下:一条SQL数据来自多个表的JOIN查询时,这个查询所属的Mapper要特别小心,因为只要其中一个表的数据发生变化,MyBatis无法通知到其他Mapper的缓存,你就可能查到过期数据。
常见疑问解答与排查思路
Q:为什么配置完二级缓存,通过日志还是能看到重复的SQL查询?
A:先检查SQLSession有没有正常提交事务,Spring中确认是否加了@Transactional注解,然后确认查询是否发生在同一个namespace的Mapper中,最后确认cacheEnabled是否为true,这三步排查能解决多数“不生效”的问题。
Q:二级缓存和一级缓存有什么区别,会互相干扰吗?
A:一级缓存是SqlSession级别的,作用于同一个会话内,默认开启,会话关闭即失效,二级缓存是Mapper级别的,跨会话共享,两者互不干扰,但要注意一级缓存在Spring整合后可能会因为SqlSession的管理方式而出现失效现象,这与二级缓存无关。
Q:配置了二级缓存之后,代码里需要做什么额外处理吗?
A:实体类实现Serializable接口,如果启用了readOnly=false,还要保证实体类有完整的getter/setter方法,序列化机制需要它们来重建对象,使用@Transactional包裹查询方法是常规操作,确保查询结果能正确写入缓存。
一个务实的结论
如果你问我现在新项目要不要配MyBatis二级缓存?我的回答是:默认不配,除非你有非常明确的理由,把缓存精力放在一级缓存调优和Redis设计上,收益会稳定得多,如果确实要用,先小范围试点,跑一周观察命中率和数据一致性,再决定是否全量推广,缓存本身就是一种权衡,用对了是性能利器,用错了就是生产事故的定时炸弹。
