参考:
https://blog.csdn.net/mingshen3/article/details/88996514
https://zhuanlan.zhihu.com/p/591469176
一、MyBatis缓存机制
Mybatis 提供了查询缓存来缓存数据,以提高查询效率。缓存级别分为一级缓存
和 二级缓存
。
1.1、一级缓存
一级缓存为 SqlSession
级别的缓存,也就是会话级缓存,是基于HashMap
的本地缓存,当同一个SqlSession
执行两次相同的SQL
语句时,第一次执行完后会将数据库中查询到的结果写到缓存,第二次查询时直接从缓存中读取,不经过数据库了。一级缓存默认是开启的。
1.2、二级缓存
二级缓存为mapper级别的缓存,多个 SqlSession
去操作同一个 Mapper 的 sql 语句,多个 SqlSession 去操作数据库得到数据会存在二级缓存区域,多个 SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的。其作用域是 mapper 的同一个 namespace
,不同的 sqlSession 两次执行相同 namespace下的 sql 语句且向 sql 中传递参数也相同即最终执行相同的 sql 语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。Mybatis 默认没有开启二级缓存需要在 setting 全局参数中配置开启二级缓存。
二、集成Redis
2.1、安装Redis
使用Docker Compose
安装Redis。docker-compose.yml
内容如下:
version: '3.1'
services:
redis:
image: redis:6.2.4
container_name: redis
restart: always
command: redis-server --requirepass 123456
ports:
- '6379:6379'
volumes:
- ./data:/data
environment:
TZ: Asia/Shanghai
安装启动完成后,可使用Redis连接工具测试
2.2、项目引入Redis
2.2.1、Maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
额外引入commons-pool2是因为data-redis底层Redis连接池基于apache commons-pool2开 发,不加入依赖会报
ClassNotFoundException
2.2.2、配置application.yml
spring:
redis:
host: 192.168.110.158
port: 6379
password: 123456
lettuce:
pool:
#最大允许连接数
max-active: 100
#最小空闲连接数,最少准备5个可用连接在连接池候着
min-idle: 5
#最大空闲连接数,空闲连接超过10个后自动释放
max-idle: 10
#当连接池到达上限后,最多等待30秒尝试获取连接,超时报错
max-wait: 30000
timeout: 2000
2.2.3、配置序列化规则
RedisTemplateConfiguration
配置类如下:
package com.micromaple.my.project.server.config;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* RedisTemplate配置
* Title: RedisTemplateConfiguration
* Description:
*
* @author Micromaple
*/
@Configuration
public class RedisTemplateConfiguration {
/**
* redisTemplate
*
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory
redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用Jackson2JsonRedisSerialize 替换默认序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
//对于Null值不输出
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 设置key和value的序列化规则
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
// 设置hashKey和hashValue的序列化规则
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
//afterPropertiesSet和init-method之间的执行顺序是afterPropertiesSet 先执行,init - method 后执行。
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
三、配置二级缓存
配置实现MyBatis二级缓存的方式有多种,比如:EhCache
、JBossCache
、Redis
,其核心原理就是客户端实现 MyBatis 提供的Cache
接口,并重写其中的方法,达到二级缓存的效果。
本文以 Redis 为例。
2.1、开启二级缓存
在 application.yml
中增加如下配置:
# 开启MyBatis二级缓存
mybatis:
configuration:
cache-enabled: true
如果使用的是 MyBatis-Plus
,则使用如下配置:
# MyBatis-Plus开启二级缓存
mybatis-plus:
configuration:
cache-enabled: true
2.2、自定义缓存类
MybatisRedisCache
缓存工具类如下:
package com.micromaple.my.project.server.utils;
import com.micromaple.my.project.server.config.ApplicationContextHolder;
import org.apache.commons.collections.CollectionUtils;
import org.apache.ibatis.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* MybatisRedisCache 缓存工具类
* Title: MybatisRedisCache
* Description:
*
* @author Micromaple
*/
public class MybatisRedisCache implements Cache {
private static final Logger logger = LoggerFactory.getLogger(MybatisRedisCache.class);
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final String id; // cache instance id
private RedisTemplate redisTemplate;
private static final long EXPIRE_TIME_IN_MINUTES = 30; // redis过期时间
public MybatisRedisCache(String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require an ID");
}
this.id = id;
}
@Override
public String getId() {
return id;
}
/**
* Put query result to redis
*
* @param key
* @param value
*/
@Override
public void putObject(Object key, Object value) {
try {
redisTemplate = getRedisTemplate();
if (value != null) {
redisTemplate.opsForValue().set(key.toString(), value, EXPIRE_TIME_IN_MINUTES, TimeUnit.MINUTES);
}
logger.debug("Put query result to redis");
} catch (Throwable t) {
logger.error("Redis put failed", t);
}
}
/**
* Get cached query result from redis
*
* @param key
* @return
*/
@Override
public Object getObject(Object key) {
try {
redisTemplate = getRedisTemplate();
logger.debug("Get cached query result from redis");
return redisTemplate.opsForValue().get(key.toString());
} catch (Throwable t) {
logger.error("Redis get failed, fail over to db", t);
return null;
}
}
/**
* Remove cached query result from redis
*
* @param key
* @return
*/
@Override
@SuppressWarnings("unchecked")
public Object removeObject(Object key) {
try {
redisTemplate = getRedisTemplate();
redisTemplate.delete(key.toString());
logger.debug("Remove cached query result from redis");
} catch (Throwable t) {
logger.error("Redis remove failed", t);
}
return null;
}
/**
* Clears this cache instance
*/
@Override
public void clear() {
redisTemplate = getRedisTemplate();
Set<String> keys = redisTemplate.keys("*:" + this.id + "*");
if (!CollectionUtils.isEmpty(keys)) {
redisTemplate.delete(keys);
}
logger.debug("Clear all the cached query result from redis");
}
/**
* This method is not used
*
* @return
*/
@Override
public int getSize() {
return 0;
}
@Override
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}
private RedisTemplate getRedisTemplate() {
if (redisTemplate == null) {
redisTemplate = ApplicationContextHolder.getBean("redisTemplate");
}
return redisTemplate;
}
}
ApplicationContextHolder
如下:
package com.micromaple.my.project.server.config;
import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* Spring bean的工具类
* Title: ApplicationContextHolder
* Description:
*
* @author Micromaple
*/
@Component
public class ApplicationContextHolder implements ApplicationContextAware, DisposableBean {
private static final Logger logger = LoggerFactory.getLogger(ApplicationContextHolder.class);
private static ApplicationContext applicationContext;
/**
* 获取存储在静态变量中的 ApplicationContext
*
* @return
*/
public static ApplicationContext getApplicationContext() {
assertContextInjected();
return applicationContext;
}
/**
* 从静态变量 applicationContext 中获取 Bean,自动转型成所赋值对象的类型
*
* @param name
* @param <T>
* @return
*/
public static <T> T getBean(String name) {
assertContextInjected();
return (T) applicationContext.getBean(name);
}
/**
* 从静态变量 applicationContext 中获取 Bean,自动转型成所赋值对象的类型
*
* @param clazz
* @param <T>
* @return
*/
public static <T> T getBean(Class<T> clazz) {
assertContextInjected();
return applicationContext.getBean(clazz);
}
/**
* 实现 DisposableBean 接口,在 Context 关闭时清理静态变量
*
* @throws Exception
*/
public void destroy() throws Exception {
logger.debug("清除 SpringContext 中的 ApplicationContext: {}", applicationContext);
applicationContext = null;
}
/**
* 实现 ApplicationContextAware 接口,注入 Context 到静态变量中
*
* @param applicationContext
* @throws BeansException
*/
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ApplicationContextHolder.applicationContext = applicationContext;
}
/**
* 断言 Context 已经注入
*/
private static void assertContextInjected() {
Validate.validState(applicationContext != null, "applicationContext 属性未注入,请在 spring-context.xml 配置中定义 SpringContext");
}
}
2.3、使用cache
在mapping中添加下面的标签
<cache eviction="LRU" type="com.kw13.testzipkin.global.MybatisRedisCache"/>
mybatis在redis中存储的key值示例如下,可以看到有sql
1830223932:3395223184:com.kw13.testzipkin.dao.KefuCallHistoryMapper.testSelect:0:2147483647:select * from kefu_call_histories where id = ?:22:SqlSessionFactoryBean
mybatis刷新缓存(就是清空缓存)
在mapper的同一个namespace中,如果有其它insert、update、delete操作数据后需要刷新缓存,如果不执行刷新缓存会出现脏读。
设置statement配置中的flushCache=‘true’ 属性,默认情况下为true即刷新缓存,如果改成false则不会刷新。使用缓存时如果手动修改数据库表中的查询数据会出现脏读。
如下:
<insertid='insertUser' parameterType='cn.itcast.mybatis.po.User' flushCache='true'>