MyBatis缓存配置
在数据库操作中,查询操作往往是占比最高、对性能影响最明显的环节。对于相同的查询请求,如果每次都直接访问数据库执行SQL,在高并发或高频查询的场景下会造成不必要的数据库压力。MyBatis内置了两层缓存机制来优化查询性能,通过缓存复用查询结果能减少与数据库的交互次数,提升系统整体响应速度。这篇笔记我们详细了解MyBatis中,一级缓存和二级缓存的概念。
一级缓存
一级缓存是MyBatis默认开启的缓存,它的作用域是单个SqlSession,在同一个SqlSession中,当执行完全相同的查询SQL时,MyBatis会优先从缓存中获取结果而不是再次访问数据库,当缓存中不存在对应结果时,MyBatis才会执行数据库查询并将结果写入缓存。一级缓存的底层实现是基于HashMap的本地内存缓存,每个SqlSession会持有独立的缓存区域,不同SqlSession之间的缓存互不干扰。
下面例子中代码执行了两个完全相同的查询,实际上这就会触发一级缓存,我们可以观察控制台上打印的日志,第二次查询时会命中缓存,不会真的再去查询数据库。
package com.gacfox.demo;
import com.gacfox.demo.mapper.UserMapper;
import com.gacfox.demo.model.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.Reader;
public class Main {
public static void main(String[] args) {
try {
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
reader.close();
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.selectUserById(1L);
System.out.println(user);
userMapper = sqlSession.getMapper(UserMapper.class);
user = userMapper.selectUserById(1L);
System.out.println(user);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
二级缓存
二级缓存也叫全局缓存,它的作用域是Mapper的Namespace级别,也就是同一个Mapper下的所有SqlSession可以共享二级缓存。不同的SqlSession执行同一个Mapper下的相同查询时可以复用二级缓存中的结果,避免重复访问数据库。和一级缓存默认开启不同,二级缓存需要手动配置开启。MyBatis内置了一种二级缓存实现,它的底层同样是基于HashMap的内存存储,默认使用PerpetualCache实现,同时MyBatis也支持集成第三方缓存框架,例如Ehcache、Redis等。
在MyBatis的mybatis-config.xml配置文件中,添加以下配置项即可开启二级缓存,如果我们没有集成额外的缓存框架,MyBatis会使用内置二级缓存的实现。
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
不过只开启二级缓存还不够,我们还需要在Mapper XML中添加缓存配置,否则二级缓存是不生效的。在Mapper XML中,我们需要添加<cache>标签,它用于开启该Mapper的缓存并配置缓存行为。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gacfox.demo.mapper.UserMapper">
<cache type="org.apache.ibatis.cache.impl.PerpetualCache" eviction="LRU" size="1024" readOnly="false"/>
<!-- ... 其它配置 -->
</mapper>
type:缓存的提供类全名,使用内置缓存时,这个值就是org.apache.ibatis.cache.impl.PerpetualCacheeviction:缓存策略,如LRU(默认),FIFO等size:缓存的引用数,默认1024个readOnly:是否为只读缓存,默认为false,只读缓存使用Map会给调用者返回相同的实例,因此这些对象不能修改否则就容易出bug,可读写缓存会通过序列化返回缓存对象的拷贝,但也因此性能不如只读缓存,但没有只读限制。此外注意,由于我们使用了可读写缓存,因此实体类必须实现Serializable接口flushInterval:缓存刷新时间,默认不自动定时刷新,只在调用语句时刷新
配置好后,我们可以通过以下代码验证缓存生效。代码执行后,第二次查询会命中缓存,我们可以看到控制台上打印Cache Hit ...日志信息。
package com.gacfox.demo;
import com.gacfox.demo.mapper.UserMapper;
import com.gacfox.demo.model.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.Reader;
public class Main {
public static void main(String[] args) {
try {
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
reader.close();
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.selectUserById(1L);
System.out.println(user);
}
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.selectUserById(1L);
System.out.println(user);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
基于EhCache实现二级缓存
MyBatis内置二级缓存实现比较简陋,实际开发中我们通常应该使用EhCache等成熟的缓存框架作为底层缓存实现。MyBatis框架支持对二级缓存的实现进行扩展,具体来说,二级缓存需要实现org.apache.ibatis.cache.Cache接口,不过这个实现类我们不必自己开发,MyBatis官方提供了和EhCache集成的库。下面例子中,我们引入了MyBatis集成EhCache的依赖。
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.3.1</version>
</dependency>
注:这里mybatis-ehcache的1.3.1版本默认集成的是EhCache2,然而EhCache实际上已经出EhCache3了,旧版本已EOL停止维护。但MyBatis官方的动作还比较缓慢,目前EhCache3支持虽然已经合入master,但相关软件包尚未发布,因此这里还是基于EhCache2进行介绍。
Mapper XML中,和之前一样,我们需要添加<cache>相关配置,不过type换成了EhCache实现。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gacfox.demo.mapper.UserMapper">
<cache type="org.mybatis.caches.ehcache.EhcacheCache" eviction="LRU" size="1024" readOnly="false"/>
<!-- ... 其它配置 -->
</mapper>
基于Redis实现二级缓存
如果我们的程序是集群方式部署的,EhCache在不同的集群节点间无法共享缓存,效率较低,此时应该使用分布式缓存。Redis是一个流行的分布式缓存服务,MyBatis官方在2015年试图实现基于Redis的二级缓存,但后来一直处于弃坑状态,直到2025年才正式发布mybatis-redis。
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.1.0</version>
</dependency>
mybatis-redis会传递依赖Jedis,我们还需要添加redis.properties配置文件,相关配置项其实就是在配置JedisConfigPool。
redis.host=localhost
redis.port=6379
redis.database=0
Mapper XML中,和之前一样,我们需要添加<cache>相关配置,不过type换成了Redis实现。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gacfox.demo.mapper.UserMapper">
<cache type="org.mybatis.caches.redis.RedisCache" eviction="LRU" size="1024" readOnly="false"/>
<!-- ... 其它配置 -->
</mapper>
实际上,我们根本不该启用MyBatis二级缓存!
其实MyBatis框架做二级缓存在今天看来并不合适,它设计上本身有很多缺点,全局、分布式下易脏读,最重要的是不够灵活,开发者并不能完全掌控缓存,而且像mybatis-ehcache、mybatis-redis这些“官方”实现社区也都规模非常小,这也侧面印证了很少有人用它们。对于缓存,我们已经有像SpringCache、JetCache这样的强大框架了,大部分工程都默认使用这些缓存框架,而非MyBatis的二级缓存,所以学完这篇笔记后你最应该添加的配置其实是下面这行。
<settings>
<setting name="cacheEnabled" value="false"/>
</settings>