Springboot 集成 mybatis 开启二级缓存(redis)
首先来了解下mybatis 缓存,mybatis缓存分为一级缓存和二级缓存。一级缓存是默认开启的,无需其他配置操作,二级缓存则需要手动设置开启。
一级缓存原理:
Mybatis的一级缓存是指同一个SqlSession中的操作。一级缓存的作用域是一个SqlSession。
在同一个SqlSession中,执行相同的查询SQL,第一次会去查询数据库,并写到缓存中;第二次直接从缓存中取。当执行SQL时两次查询中间发生了增删改操作,则SqlSession的缓存清空。
二级缓存原理:
Mybatis的二级缓存是指mapper映射文件。二级缓存是多个sqlSession共享的,其作用域是mapper下的同一个namespace。
在不同的sqlSession中,相同的namespace下,相同的查询sql语句并且参数也相同的情况下,会命中二级缓存。如果调用相同namespace下的mapper映射文件中的增删改SQL,并执行了commit操作。此时会清空该namespace下的二级缓存。
了解一些基本原理后,我们开始在springboot集成mybatis的情况下,开启二级缓存。
- 在pom.xml文件中引入mybatis和redis的依赖
<!--mybatis 依赖包-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--redis lettuce-->
<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>
- 在application.yml文件中配置mybatis相关设置时,开启二级缓存
### mybatis相关配置
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#开启MyBatis的二级缓存
cache-enabled: true
mapper-locations: classpath*:mappers/*Mapper.xml
### Redis 相关配置
redis:
host: localhost
port: 6379
timeout: 10000
database: 0
lettuce:
pool:
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 0
- 实体类实现序列化
我们采用的redis序列化方式是默认的jdk序列化。所以数据库的查询对象(比如Student类)需要实现Serializable接口。
public class Student implements Serializable {
//采用的redis序列化方式是默认的jdk序列化。所以数据库的查询对象实体需要实现Serializable接口。
private static final long serialVersionUID = 1L;
private int id;
private String name;
private int age;
private String position;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getPosition() {
return position;
}
public void setPosition(String position) {
this.position = position;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", position='" + position + '\'' +
'}';
}
}
- 先看一下Redis的配置类(这里用的是lettuce)
@Configuration
public class RedisConfig {
@Autowired
private LettuceConnectionFactory connectionFactory;
@Bean
public RedisTemplate<String,Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
initDomainRedisTemplate(redisTemplate, connectionFactory);
return redisTemplate;
}
/**
* 设置数据存入 redis 的序列化方式
* @param template
* @param factory
*/
private void initDomainRedisTemplate(RedisTemplate<String, Object> template,LettuceConnectionFactory factory) {
// 定义 key 的序列化方式为 string
// 需要注意这里Key使用了 StringRedisSerializer,那么Key只能是String类型的,不能为Long,Integer,否则会报错抛异常。
StringRedisSerializer redisSerializer = new StringRedisSerializer();
template.setKeySerializer(redisSerializer);
// 定义 value 的序列化方式为 json
@SuppressWarnings({"rawtypes", "unchecked"})
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setValueSerializer(jackson2JsonRedisSerializer);
//hash结构的key和value序列化方式
template.setHashKeySerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.setEnableTransactionSupport(true);
template.setConnectionFactory(factory);
}
}
- 缓存配置类
public class MybatisRedisCache implements Cache {
private static final Logger log = LoggerFactory.getLogger(MybatisRedisCache.class);
private String id;
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//private static final long EXPIRE_TIME_IN_MINUTES = 30; // redis过期时间
public MybatisRedisCache(String id) {
this.id = id;
}
private RedisTemplate<Object, Object> getRedisTemplate(){
return ApplicationContextHolder.getBean("redisTemplate");
}
@Override
public String getId() {
return id;
}
@Override
public void putObject(Object key, Object value) {
RedisTemplate redisTemplate = getRedisTemplate();
redisTemplate.boundHashOps(getId()).put(key, value);
log.info("[结果放入到缓存中: " + key + "=" + value+" ]");
}
@Override
public Object getObject(Object key) {
RedisTemplate redisTemplate = getRedisTemplate();
Object value = redisTemplate.boundHashOps(getId()).get(key);
log.info("[从缓存中获取了: " + key + "=" + value+" ]");
return value;
}
@Override
public Object removeObject(Object key) {
RedisTemplate redisTemplate = getRedisTemplate();
Object value = redisTemplate.boundHashOps(getId()).delete(key);
log.info("[从缓存删除了: " + key + "=" + value+" ]");
return value;
}
@Override
public void clear() {
RedisTemplate redisTemplate = getRedisTemplate();
redisTemplate.delete(getId());
log.info("清空缓存!!!");
}
@Override
public int getSize() {
RedisTemplate redisTemplate = getRedisTemplate();
Long size = redisTemplate.boundHashOps(getId()).size();
return size == null ? 0 : size.intValue();
}
@Override
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}
}
ps:
重点部分就是重写这个mybatis的cache类,它只会对配置文件类型的映射文件起作用。
该接口共有以下五个方法:
String getId():mybatis缓存操作对象的标识符。一个mapper对应一个mybatis的缓存操作对象。
void putObject(Object key, Object value):将查询结果塞入缓存。
Object getObject(Object key):从缓存中获取被缓存的查询结果。
Object removeObject(Object key):从缓存中删除对应的key、value。只有在回滚时触发。
void clear():发生更新时,清除缓存。
int getSize():可选实现。返回缓存的数量。
ReadWriteLock getReadWriteLock():可选实现。用于实现原子性的缓存操作。
上述重写cache类中有几个关键点:
- 自定义实现的二级缓存,必须要有一个带id的构造函数,否则会报错。
- 此处使用Spring封装的redisTemplate来操作Redis。很多都是直接用jedis库,但是现在springboot2.x 以上对lettuce的兼容更好。RedisTemplate封装了底层的实现,使用redisTemplate会更加方便,无论是使用jedis还是使用lettuce,我们可以直接更换底层的库,无需修改上层代码。
- 这里不能通过@Autowire的方式引用redisTemplate,因为RedisCache并不是Spring容器里的bean。所以我们需要手动地去调用容器的getBean方法来拿到这个bean,那么这样,我们就需要引入ApplicationContextHolder这个类。
- ApplicationContextHolder.java (我们需要通过这个类得到RedisTemplate)
@Component
public class ApplicationContextHolder implements ApplicationContextAware{
private static ApplicationContext applicationContext;
/**
* 实现ApplicationContextAware接口的context注入函数, 将其存入静态变量.
*/
public void setApplicationContext(ApplicationContext applicationContext) {
ApplicationContextHolder.applicationContext = applicationContext; // NOSONAR
}
/**
* 取得存储在静态变量中的ApplicationContext.
*/
public static ApplicationContext getApplicationContext() {
checkApplicationContext();
return applicationContext;
}
/**
* 从静态变量ApplicationContext中取得Bean, 自动转型为所赋值对象的类型.
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(String name) {
checkApplicationContext();
return (T) applicationContext.getBean(name);
}
/**
* 从静态变量ApplicationContext中取得Bean, 自动转型为所赋值对象的类型.
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(Class<T> clazz) {
checkApplicationContext();
return (T) applicationContext.getBeansOfType(clazz);
}
/**
* 清除applicationContext静态变量.
*/
public static void cleanApplicationContext() {
applicationContext = null;
}
private static void checkApplicationContext() {
if (applicationContext == null) {
throw new IllegalStateException("applicaitonContext未注入,请在applicationContext.xml中定义SpringContextHolder");
}
}
}
- 然后再映射文件中开启二级缓存(使用二级缓存)
<mapper namespace="com.example.demo.dao.StudentDao">
<!-- 开启基于redis的二级缓存 -->
<cache type="com.example.demo.redis.cache.MybatisRedisCache"/>
<cache/>
<insert id="insert" parameterType="com.example.demo.entity.Student" useGeneratedKeys="true" keyProperty="id">
insert into
students(name,age,position)
values
(#{name},#{age},#{position})
</insert>
<insert id="batchInsert" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="id">
insert into
students(name,age,position)
values
<foreach collection="studentList" item="item" index="index" open="" close="" separator=",">
(
#{item.name},
#{item.age},
#{item.position}
)
</foreach>
</insert>
<delete id="delete" parameterType="java.lang.String">
delete from students where name = #{name}
</delete>
<!--并且在update语句中,设置flushCache为true,这样在更新信息时,能够自动失效缓存(本质上调用的是clear方法)-->
<update id="update" parameterType="com.example.demo.entity.Student" flushCache="true">
update students
set students.position = #{position}
where name = #{name}
</update>
<select id="findByName" resultMap="BaseResultMap">
select *
from students
where name = #{name}
</select>
<select id="findAll" resultMap="BaseResultMap">
select *
from students
</select>
<resultMap id="BaseResultMap" type="com.example.demo.entity.Student">
<result column="name" property="name"/>
<result column="age" property="age"/>
<result column="position" property="position"/>
</resultMap>
</mapper>
下面是我在实现二级缓存过程中一些报错问题:
- 在我修改了序列化问题后,报错消失。
小白入门,在实践过程中参考了很多优秀的博客:
https://my.oschina.net/ljc94/blog/1504320
https://blog.****.net/qq_40008535/article/details/83446123