微信优化

weixinyouhua

MyBatis缓存配置有哪些常见问题,MyBatis缓存配置怎么解决?

2026-08-20 18:26:09

MyBatis缓存配置的核心结论是:一级缓存默认开启且作用域为SqlSession,二级缓存需要在全局配置和Mapper映射文件中双重开启,且实体类必须实现序列化,否则缓存会静默失效。

mybatis一级缓存和二级缓存的区别在哪里

很多初次接触MyBatis的开发者容易混淆两级缓存的作用边界,从实际运行机制来看,一级缓存是SqlSession级别的本地缓存,它默认开启,无法通过配置直接关闭,当你使用同一个SqlSession对象执行两次完全相同的查询时,第二次查询会直接命中缓存,不再访问数据库,这就解释了为什么在循环中反复查询同一条数据时,日志里只出现一条SQL语句。

二级缓存则完全不同,它的作用域是Mapper的Namespace级别,也就是同一个Mapper接口下的所有查询语句共享同一个缓存区域,二级缓存默认是关闭的,需要开发者在mybatis-config.xml配置文件中将cacheEnabled设置为true,同时在具体的Mapper.xml文件中添加<cache/>标签,两者缺一不可。

从使用场景来看,一级缓存适合单次业务请求内的重复查询优化,比如一个方法中多次查询字典数据,而二级缓存适合跨请求、跨SqlSession的共享数据缓存,比如用户信息、商品分类这类变动频率较低的数据,行业共识认为,二级缓存设计不当是导致脏读的头号原因,开启前必须评估数据的更新频率和一致性要求。

mybatis二级缓存配置失效的常见原因

实体类没有实现Serializable接口

这是踩坑率最高的一个环节,MyBatis二级缓存默认使用序列化方式存储对象,如果实体类没有实现java.io.Serializable接口,程序不会直接报错,而是缓存直接失效,每次查询仍然访问数据库,很多团队排查半天找不到原因,最后发现是实体类忘了加序列化接口。

缓存标签配置过于简短

在Mapper.xml中直接写<cache/>虽然语法正确,但使用的全是默认值,默认的回收策略是LRU(最近最少使用),默认的刷新间隔是0(也就是不主动刷新),默认的缓存大小是1024个引用,这些默认值在低并发场景下问题不大,但在接口压力较高时,缓存命中率会明显下降。

多表关联查询导致缓存数据不一致

MyBatis缓存配置有哪些常见问题,MyBatis缓存配置怎么解决?

二级缓存是按Namespace隔离的,假设订单查询SQL关联了用户表,订单Mapper的缓存中存了用户的部分字段快照,当用户表的数据发生变更时,用户Mapper的缓存区域会被清空,但订单Mapper的缓存区域并不知道这件事,此时再次查询订单,取到的仍是旧的用户信息,这是二级缓存架构设计上的天然缺陷,最直接的解决方案是:涉及多表关联的查询,一律不要在Mapper中开启二级缓存,只对单表独立查询使用二级缓存。

如何设置mybatis缓存配置文件

完整的二级缓存配置涉及三个层面的操作,下面拆开细说。

第一步:全局配置开启缓存开关

mybatis-config.xml主配置文件中添加如下设置:

<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>

这里要注意,cacheEnabled默认值就是true,所以这一行其实可以不写,但为了让团队成员一眼看懂项目显式声明了开启二级缓存,建议保留这段配置。

第二步:在Mapper.xml中启用当前Namespace的缓存

<mapper namespace="com.example.mapper.UserMapper">
    <cache
        eviction="LRU"
        flushInterval="60000"
        size="512"
        readOnly="false"/>
</mapper>

各个参数的含义需要逐一说明:

  • eviction:缓存回收策略,LRU表示移除闲置最久的对象,FIFO表示按进入顺序移除。
  • flushInterval:缓存刷新间隔,单位是毫秒,设置60000表示每60秒自动清空一次缓存,设置0表示不主动刷新,只有执行增删改语句时才会清空。
  • size:缓存可存储的对象引用数量,不宜设得过大,否则内存压力过高。
  • readOnly:只读模式下所有使用者拿到同一个缓存对象,性能高但有并发修改风险,非只读模式会通过序列化复制出新对象,安全但开销稍大,多数情况下建议设为false,避免调用方修改对象内容污染缓存。

第三步:实体类实现序列化接口

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private Long id;
    private String username;
    // 其他字段与方法
}

MyBatis缓存配置有哪些常见问题,MyBatis缓存配置怎么解决?

serialVersionUID建议显式声明,否则每次修改类结构时Java编译器都会生成新的版本号,可能导致反序列化失败,这类问题在分布式环境下排查起来相当棘手。

分布式环境下mybatis与redis的二级缓存整合方案

如果你的应用部署了多个实例,且使用了nginx做负载均衡,那么MyBatis自带的本地二级缓存基本失去意义,每个实例持有各自的缓存区域,A实例更新了数据,B实例的缓存仍然保留旧值,此时需要用分布式缓存替换掉LocalCache。

实现方式非常直接:实现org.apache.ibatis.cache.Cache接口,在实现类中调用RedisTemplate或Jedis完成数据读写,核心代码逻辑大致如下:

public class RedisCache implements Cache {
    private final String id;
    private RedisTemplate<Object, Object> redisTemplate;
    public RedisCache(String id) {
        this.id = id;
        this.redisTemplate = SpringContextHolder.getBean("redisTemplate");
    }
    @Override
    public void putObject(Object key, Object value) {
        redisTemplate.opsForHash().put(id, key.toString(), value);
    }
    @Override
    public Object getObject(Object key) {
        return redisTemplate.opsForHash().get(id, key.toString());
    }
    // 省略其他接口方法
}

然后在Mapper.xml中指定缓存实现类:

<cache type="com.example.config.RedisCache"/>

这种做法的好处是多个实例共享同一个Redis缓存,数据一致性由Redis保证,但要注意,缓存Key的Namespace隔离仍然生效,不同Mapper之间不会互相访问缓存数据。

使用mybatis缓存时需要注意什么

增删改操作自动失效缓存

MyBatis的设计中,任何针对当前Namespace的INSERT、UPDATE、DELETE操作都会触发缓存清空,这是避免脏读的关键机制,开发时不应该手动去调用SqlSession的clearCache方法,而是依赖框架自身的失效策略,但getMapper接口方式调用时,需要注意批量插入场景下缓存被频繁清空造成的性能损耗。

查询参数必须实现equals和hashCode

二级缓存的Key是由查询参数对象组合生成的,如果参数对象没有正确实现

MyBatis缓存配置有哪些常见问题,MyBatis缓存配置怎么解决?

equalshashCode方法,即使传入的字段值完全相同,也会被判定为不同的Key,导致缓存永远无法命中,对于使用Map作为参数的情况,HashMap本身已经实现了这两个方法,通常没有问题。

缓存命中率如何判断

可以在MyBatis的日志配置中开启LoggingCache来查询缓存命中情况,配置日志级别为DEBUG后,日志中会出现Cache Hit Ratio字样,这个比例数值能直观反映缓存是否有效,如果长期低于50%,说明缓存的key设计不合理或数据更新过于频繁,这时反而应该考虑关闭二级缓存,减轻维护负担。

mybatis缓存常见问题解答

一级缓存为什么在Spring整合后经常失效

因为Spring整合MyBatis后,每次执行Mapper方法都会创建新的SqlSession,方法结束则关闭会话,一级缓存的生命周期与SqlSession绑定,会话关闭缓存就销毁了,所以在一级缓存整个生命周期内只能作用在单次数据库操作中,跨方法复用根本不现实,如果需要在Service层多次查询共享缓存,请直接配置二级缓存。

使用MyBatis-Plus框架时如何开启二级缓存

MyBatis-Plus本质上是对MyBatis的增强,开启方式与原生MyBatis完全一致,在实体类上添加@TableName注解并实现Serializable接口,在Mapper接口对应的XML中配置<cache/>标签即可,需要注意MyBatis-Plus自带的分页插件生成的COUNT查询统计SQL也会走二级缓存逻辑,在分页数据变化频繁的场景下可能产生统计结果不准的问题,此时可以在分页查询方法上使用skipCache注解绕过缓存。

线上环境开启二级缓存后出现脏数据,最快速的临时处理方案是什么

最快的方案是通过配置中心动态下发开关,将涉及多表关联的Mapper的cacheEnabled设置为false,同时重启应用清空已有缓存,如果公司没有配置中心,则可以在MyBatis配置文件中注释掉<cache/>标签,重新打包发布,后续再排查具体的关联表和缓存冲突源头,据统计,大多数线上缓存脏读问题都出在关联SQL写入非本表Mapper的缓存区域,治理思路应以限制缓存使用范围为主,而非提高缓存刷新频率。

相关文章

2024年,SaaS软件行业碰到获客难、增长慢等问题吗?

我们努力让每一次邂逅总能超越期待