引言
所在团队使用 SpringBoot 框架开发后端程序,选用 MyBatis-Plus 作为 ORM 框架,在单实例服务中没有出现过缓存问题。默认缓存逻辑是:查询数据库后,框架自动将结果缓存,下次查询可以从缓存中返回、不访问数据库;一旦出现增、删、改操作则清空缓存。
如果服务是多实例的,不应开启缓存,由于实例间没有协调,这种缓存机制会造成脏读!!!
MyBatis 缓存机制
MyBatis-Plus 提供了两个级别的缓存,都是基于 MyBatis 缓存的封装,而 MyBatis 提供了三种类型的缓存:
- Local Cache(本地缓存):默认开启的一级缓存,它仅在同一个 SQLSession 内有效,通过 HashMap 实现。当执行相同的查询时,MyBatis 将直接返回缓存的对象,而不会再向数据库发送 SQL 查询语句。
- Second Level Cache(二级缓存):默认关闭的全局缓存,可以跨 SQLSession 和事务使用。这种缓存需要进行配置,并且支持多种缓存实现(如 Ehcache、Redis 等)。二级缓存可以缓存查询结果或映射的对象,使得相同的查询可以跨不同的 SQLSession 共享缓存数据。
- 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]
...二级缓存
配置缓存
为排除一级缓存影响,这里先“关闭” 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
...