后端程序使用 MyBatis-Plus 缓存

引言

所在团队使用 SpringBoot 框架开发后端程序,选用 MyBatis-Plus 作为 ORM 框架,在单实例服务中没有出现过缓存问题。默认缓存逻辑是:查询数据库后,框架自动将结果缓存,下次查询可以从缓存中返回、不访问数据库;一旦出现增、删、改操作则清空缓存。

MyBatis 缓存机制

MyBatis-Plus 提供了两个级别的缓存,都是基于 MyBatis 缓存的封装,而 MyBatis 提供了三种类型的缓存:

  1. Local Cache(本地缓存):默认开启的一级缓存,它仅在同一个 SQLSession 内有效,通过 HashMap 实现。当执行相同的查询时,MyBatis 将直接返回缓存的对象,而不会再向数据库发送 SQL 查询语句。
  2. Second Level Cache(二级缓存):默认关闭的全局缓存,可以跨 SQLSession 和事务使用。这种缓存需要进行配置,并且支持多种缓存实现(如 Ehcache、Redis 等)。二级缓存可以缓存查询结果或映射的对象,使得相同的查询可以跨不同的 SQLSession 共享缓存数据。
  3. Custom Cache(自定义缓存):可以通过实现 Cache 接口来创建自定义的缓存策略,例如将缓存存储在特定的内存数据库或外部存储系统中。

缓存只针对于DQL语句,也就是说缓存机制只对应select语句。且只要两次查询之间出现了增、删、改操作,就会清空缓存。

在单服务架构中(仅有一个程序提供相同服务),开启缓存不会影响业务,只会提高性能。
在微服务架构中需要关闭缓存,原因是:Service1 查询数据后,如果 Service2 修改了数据,Service1 再次查询时可能会得到过期数据。

MyBatis-Plus 缓存配置

一级缓存

配置缓存

MyBatis-Plus默认开启了一级缓存,无需额外配置即可测试一级缓存效果。

测试缓存

在一个 SQL Session 中,使用同一个查询两次。

@Test
@Transactional // 保证在一个SQL Session中。注释本行则不会触发一级缓存,日志中将显示两次访问数据库。
void cacheTest() throws InterruptedException { // 测试MyBatis缓存
    Pager<Carrier> pager = carrierRepository.page( // 首次查询
            new CarrierPageQuery(
                    1, 1000,
                    null, null, List.of(EntityStatusEnum.ENABLE, EntityStatusEnum.DISABLE),
                    null, null,
                    null,null, null)
    );
    System.out.println(pager);
    Pager<Carrier> pager2 = carrierRepository.page( // 以相同条件再次查询
            new CarrierPageQuery(
                    1, 1000,
                    null, null, List.of(EntityStatusEnum.ENABLE, EntityStatusEnum.DISABLE),
                    null, null,
                    null,null, null)
    );
    System.out.println(pager2);
}

输出日志如下,只执行了一次 SQL select 操作,一级缓存生效。

Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7b7b1448]
JDBC Connection [com.alibaba.druid.pool.DruidStatementConnection@11180750] will be managed by Spring
==>  Preparing: SELECT COUNT(*) AS total FROM tbl_carrier WHERE (status IN (?, ?))
==> Parameters: 0(Integer), 1(Integer)
<==    Columns: total
<==        Row: 2
<==      Total: 1
==>  Preparing: SELECT id,type,type_prefix,config,name,description,extension,status,created_time,last_updated_time FROM tbl_carrier WHERE (status IN (?,?)) LIMIT ?
==> Parameters: 0(Integer), 1(Integer), 1000(Long)
<==    Columns: id, type, type_prefix, config, name, description, extension, status, created_time, last_updated_time
<==        Row: 7226787016, 202, SMS, <<BLOB>>, 腾讯云sms-访客违章通知, null, <<BLOB>>, 0, 2025-07-10 16:43:40, 2025-07-10 16:43:40
<==        Row: 46271564755, 100, EMAIL, <<BLOB>>, 邮件1, null, <<BLOB>>, 0, 2025-07-10 16:43:40, 2025-07-10 16:43:40
<==      Total: 8
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7b7b1448]
...

二级缓存

配置缓存

使用配置 | localCacheScope

常见问题 | mapper-层二级缓存问题

为排除一级缓存影响,这里先“关闭” MyBatis 一级缓存,再开启二级缓存。配置过程如下:

MyBatis 一级缓存默认开启而且不能关闭,这里其实是把缓存的作用域从 session 修改到 statement。

原因见:

Local cache is used by some basic functionality (association/collection mapping, circular references). Besides, the current result mapping code heavily relies on CacheKey, so it may be quite difficult (if possible) to add such option

https://github.com/mybatis/mybatis-3/issues/1278

  • 修改pom.xml,引入 Redis 依赖
<!-- Spring Data Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • 修改application.yml配置文件,关闭一级缓存、开启二级缓存,配置 Redis 地址和密码
mybatis-plus:
  configuration:
    local-cache-scope: statement # 关闭一级缓存
    cache-enabled: true # 开启二级缓存
spring:  
  data:
    redis:
      host: localhost
      port: 6379
      password: redispassword
  • 定义工具类,便于在非 Spring 管理的类中获取 Bean。
    • Cache 实现类是 MyBatis 自己创建实例并管理的,不会从 Spring 容器中获取,所以 Cache 类不能靠 Spring 注入依赖。
package cc.synx.msgAnno.utility;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * 在非 Spring 管理的类中获取 Bean
 */
@Component
public class SpringContextHolder implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext context) throws BeansException {
        applicationContext = context;
    }

    // 根据名称获取Bean
    public static Object getBean(String name) {
        return applicationContext.getBean(name);
    }

    // 根据名称和类型获取Bean
    public static <T> T getBean(String name, Class<T> clazz) {
        return applicationContext.getBean(name, clazz);
    }
  • 定义 Cache 实现类
package cc.synx.msgAnno.infrastructure.rpc;

import cc.synx.msgAnno.utility.SpringContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.Cache;
import org.springframework.data.redis.connection.RedisServerCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;

import java.util.Set;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

@Slf4j
public class MybatisRedisCache implements Cache {

    // 读写锁
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);

//    @Resource // MyBatis会自己创建Cache实例,而不是从Spring容器中获取,所以这里也无法注入
    private RedisTemplate<String, Object> redisTemplate;

    private final String id;

    public MybatisRedisCache(String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require an ID");
        }
        this.id = id;
    }

    @Override
    public String getId() {
        return this.id;
    }

    @Override
    public void putObject(Object key, Object value) {
        if (redisTemplate == null) // Spring容器初始化完成后,查询redisTemplate
            redisTemplate = (RedisTemplate<String, Object>) SpringContextHolder.getBean("redisTemplate");

        if (value != null) {
            redisTemplate.opsForValue().set(key.toString(), value);
        }
    }

    @Override
    public Object getObject(Object key) {
        if (redisTemplate == null) // Spring容器初始化完成后,查询redisTemplate
            redisTemplate = (RedisTemplate<String, Object>) SpringContextHolder.getBean("redisTemplate");

        try {
            if (key != null) {
                return redisTemplate.opsForValue().get(key.toString());
            }
        } catch (Exception e) {
            log.error("缓存出错 :", e);
        }
        return null;
    }

    @Override
    public Object removeObject(Object key) {
        if (key != null) {
            redisTemplate.delete(key.toString());
        }
        return null;
    }

    @Override
    public void clear() {
        log.debug("清空缓存");
        Set<String> keys = redisTemplate.keys("*:" + this.id + "*");
        if (!CollectionUtils.isEmpty(keys)) {
            redisTemplate.delete(keys);
        }
    }

    @Override
    public int getSize() {
        Long size = redisTemplate.execute(RedisServerCommands::dbSize);
        return size.intValue();
    }

    @Override
    public ReadWriteLock getReadWriteLock() {
        return this.readWriteLock;
    }
}
  • 在需要的 Mapper 上面,加上注解@CacheNamespace(implementation= MybatisRedisCache.class),指定使用的 Cache 实现。
package cc.synx.msgAnno.infrastructure.persistence.mapper.job;

import cc.synx.msgAnno.infrastructure.rpc.MybatisRedisCache;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.CacheNamespace;
import org.apache.ibatis.annotations.Mapper;

import cc.synx.msgAnno.infrastructure.persistence.po.job.CarrierPO;

@Mapper
@CacheNamespace(implementation= MybatisRedisCache.class)
public interface CarrierMapper extends BaseMapper<CarrierPO> {

}

测试缓存

使用前文的测试代码,注释掉@Transactional,启动两个程序实例执行。

@Test
// @Transactional // 注释本行,因为在事务中不会写入二级缓存,事务结束才会写入。
void cacheTest() throws InterruptedException { // 测试MyBatis缓存
    Pager<Carrier> pager = carrierRepository.page( // 首次查询
            new CarrierPageQuery(
                    1, 1000,
                    null, null, List.of(EntityStatusEnum.ENABLE, EntityStatusEnum.DISABLE),
                    null, null,
                    null,null, null)
    );
    System.out.println(pager);
    Pager<Carrier> pager2 = carrierRepository.page( // 以相同条件再次查询
            new CarrierPageQuery(
                    1, 1000,
                    null, null, List.of(EntityStatusEnum.ENABLE, EntityStatusEnum.DISABLE),
                    null, null,
                    null,null, null)
    );
    System.out.println(pager2);
}
  • 实例A。首次查询访问了数据库,第二次相同查询从缓存返回。
Creating a new SqlSession
...
Cache Hit Ratio [cc.synx.msgAnno.infrastructure.persistence.mapper.job.CarrierMapper]: 0.0
==>  Preparing: SELECT id,type,type_prefix,config,name,description,extension,status,created_time,last_updated_time FROM tbl_carrier WHERE (status IN (?,?)) LIMIT ?
==> Parameters: 0(Integer), 1(Integer), 1000(Long)
<==    Columns: id, type, type_prefix, config, name, description, extension, status, created_time, last_updated_time
<==        Row: 7226787016, 202, SMS, <<BLOB>>, 腾讯云sms-访客违章通知, null, <<BLOB>>, 0, 2025-07-10 16:43:40, 2025-07-10 16:43:40
<==        Row: 46271564755, 100, EMAIL, <<BLOB>>, 邮件1, null, <<BLOB>>, 0, 2025-07-10 16:43:40, 2025-07-10 16:43:40
<==      Total: 2
...

Creating a new SqlSession
...
Cache Hit Ratio [cc.synx.msgAnno.infrastructure.persistence.mapper.job.CarrierMapper]: 0.5
...
  • 实例B。Redis中已经被实例A缓存了查询结果,因此没有访问数据库。
Creating a new SqlSession
...
Cache Hit Ratio [cc.synx.msgAnno.infrastructure.persistence.mapper.job.CarrierMapper]: 1.0
...

Creating a new SqlSession
...
Cache Hit Ratio [cc.synx.msgAnno.infrastructure.persistence.mapper.job.CarrierMapper]: 1.0
...

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

Index
滚动至顶部