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.PerpetualCache
  • eviction:缓存策略,如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-ehcache1.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-ehcachemybatis-redis这些“官方”实现社区也都规模非常小,这也侧面印证了很少有人用它们。对于缓存,我们已经有像SpringCache、JetCache这样的强大框架了,大部分工程都默认使用这些缓存框架,而非MyBatis的二级缓存,所以学完这篇笔记后你最应该添加的配置其实是下面这行。

<settings>
    <setting name="cacheEnabled" value="false"/>
</settings>
作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。