mybatis结合redis实现自定义缓存
- 缓存的应运而生
众所周知呢,在实际项目中,频繁操作数据库是十分耗费资源的。这个时候,缓存的出现就在一定程度上解决了这种问题。这里为什么说是一定程度上呢:因为缓存的主要优势体验在查询操作非常频繁的场景下[我们将一次查询的结果放入缓存中,当我们再次查询相同的数据的时候,直接走缓存,就不再走数据库了],如果一个场景修改数据非常频繁,那缓存就几乎起不到优势作用了。
- 下面介绍一下什么是缓存:
缓存的英文是cache,一般是用于RAM存储器,用于存储临时数据,断电后存储的内容会消失。
缓存是临时文件交换区,电脑把最常用的文件从存储器里提出来临时放在缓存里,就像把工具和材料搬上工作台一样,这样会比用时现去仓库取更方便。
将用户经常查询的数据放在缓存(内存)中,用户去查询数据就不用从磁盘上查询,而是直接从缓存中查询,从而提高了查询的效率,解决了高并发系统的性能问题。
- 下面介绍mybatis的缓存:
mybatis拥有一级缓存和二级缓存这两种机制:
一级缓存是SqlSession级别的缓存
如上图代码所示:一次sqlSession的开与关就表示了一次缓存,在开与关之间进行多次查询的话,那么只会有一次查询数据库;如果两次查询之间夹杂着一次更新操作,那么这两次查询都会走数据库。虽然一级缓存是默认开启的,但是在我们实际的使用过程中并没有感受到其带来的方便之处。
二级缓存是mapper级别的缓存
二级缓存是基于 mapper文件的namespace的,也就是说多个sqlSession可以共享一个mapper中的二级缓存区域,并且如果两个mapper的namespace相同,即使是两个mapper,那么这两个mapper中执行sql查询到的数据也将存在相同的二级缓存区域中。
如上图所示,二级缓存在一个mapper的namespace下横跨在多个一级缓存上。下面演示一下,二级缓存 在关闭和开启时的效果:
在没有开启二级缓存的时候,效果如下:
在同一个测试方法下,执行两次查询方法,那么这两次查询方法都会对数据库进行查询。当开启二级缓存后:
效果如下:
这里需要强调几点问题:
以上测试都是在springboot中进行的,在yml文件中对mybatis进行配置的时候,需要注意,仅仅在yml中配置了cache-enabled: true 是不起作用的,需要在mapper.xml文件中同时加入标签,这样二级缓存才会开启。
server:
port: 8081
# 数据库基本配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.31.100:3306/mybatis?useSSL=false&serverTimezone=UTC
username: root
password: xxxxxx
type: com.alibaba.druid.pool.DruidDataSource
# mybatis相关配置
mybatis:
configuration:
map-underscore-to-camel-case: true
cache-enabled: true
type-aliases-package: com.sample.entity
mapper-locations: classpath:mapper/*.xml
# 日志配置
logging:
level:
com.sample.dao: debug
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"
"http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">
<mapper namespace="com.sample.dao.IEmployeeDao">
<cache/><!--开启二级缓存-->
<select id="findAll" resultType="employee">
select * from employee
</select>
<select id="findById" parameterType="integer" resultType="employee">
select * from employee where id = #{id}
</select>
</mapper>
虽然mybatis的二级缓存已经比较好用了,但是并没有解决断电数据消失的问题。只要电脑一断电,那么缓存中的数据就不存在了。
此外,当在集群环境下,应用部署在多个服务器上,那么根据负载均衡的原理,查询操作可能会请求不同的服务器,那么就会出现这样的问题,在服务器1上的查询已经放入了服务器1的缓存中,第二次查询可能请求服务器2,而服务器2就要先查询数据库,然后再把数据放入服务器2的缓存中。如此一来,十分的麻烦。所以这里就需要使用分布式缓存对这个问题进行解决。
所以我们要接触第三方工具去解决以上问题,本篇文章就来介绍mybatis配合redis进行二级缓存的使用。
由于redis拥有持久化机制,所以断电数据消失的问题就可以借助redis来解决。
mybatis提供了Cache接口,用于实现自定义的缓存策略。
在实现该接口前,我们首先看看mybatis的默认缓存实现是怎么样的:
public class PerpetualCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap<>();
public PerpetualCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public int getSize() {
return cache.size();
}
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return cache.get(key);
}
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}
@Override
public void clear() {
cache.clear();
}
@Override
public boolean equals(Object o) {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
if (this == o) {
return true;
}
if (!(o instanceof Cache)) {
return false;
}
Cache otherCache = (Cache) o;
return getId().equals(otherCache.getId());
}
@Override
public int hashCode() {
if (getId() == null) {
throw new CacheException("Cache instances require an ID.");
}
return getId().hashCode();
}
}
从源码中我们不难看出,mybatis的默认缓存实现,内部维护了一个HashMap,最常用的方法是putObject和getObject
我们通过断点可以知道,在putObject中,key是mapper中namespace+标签的id
value是一个ArrayList集合,其中保存了我们查询到的结果。
接下来我们就针对这两个方法进行缓存的定制。
public class CustomRedisCache implements Cache {
/*
* id是必须的带上的,这里的id会指定当前放入缓存的mapper的namespace
* 如这里的id就是com.sample.dao.IEmployeeDao
*/
private final String id;
public CustomRedisCache(String id) {
this.id = id;
}
@Override
public String getId() {
return id;
}
……
}
在mapper中需要改成下面这样
<cache type="com.sample.config.CustomRedisCache"/>
到这里我们就要注意了,在CustomRedisCache这个类中,我们该如何加入redis的实例。CustomRedisCache实例化并不是由工厂或者spring容器进行的,而是由mybatis进行的。因为在CustomRedisCache实例化时,mybatis需要向其构造函数中传入namespace。
所以在CustomRedisCache类中,不能使用注入的方式向其添加redis实例。
那么我们就要另辟蹊径了。在学习spring的时候,我们知道ApplicationContext是最大的容器,然后通过其getBean方法来获取redis实例。
下面我们写一个工具类来获取ApplicationContext.
@Configuration
public class ApplicationContextUtil implements ApplicationContextAware {
//通俗来讲,这里的applicationContext就是spring为我们保留下来的工厂
static ApplicationContext applicationContext;
/*
* 实现了ApplicationContextAware接口后,Spring容器会自动把上下文环境对象
* 调用ApplicationContextAware接口中的setApplicationContext方法进行设置
* */
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
ApplicationContextUtil.applicationContext = applicationContext;
}
//通过在工厂中获取对象的方法
/* public static Object getBean(String beanName){
return applicationContext.getBean(beanName);
}*/
//将上面的方法使用泛型进行优化;因为上面那个方法需要强制类型转换
public static <T> T getBean(String beanName,Class<T> requiredType){
return applicationContext.getBean(beanName,requiredType);
}
}
下面是在CustomRedisCache添加redis及相关操作
public class CustomRedisCache implements Cache {
/*
* id是必须的带上的,这里的id会指定当前放入缓存的mapper的namespace
* 如这里的id就是com.sample.dao.IEmployeeDao
*/
private final String id;
private final RedisTemplate redisTemplate;
public CustomRedisCache(String id) {
// 获取redis实例
redisTemplate = ApplicationContextUtil.getBean("redisTemplate",RedisTemplate.class);
// 指定key的序列化方式
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public void putObject(Object key, Object value) {
redisTemplate.opsForHash().put(id.toString(),key.toString(),value);
}
@Override
public Object getObject(Object key) {
return redisTemplate.opsForHash().get(id.toString(),key.toString());
}
...
}
运行测试案例
如下所示,结果显示成功
redis中保有数据
当我们再次执行刚才的测试案例,那么这两次数据查询都不会走数据库,而是都走缓存。
到此为止,mybatis+redis实现二级缓存的例子就结束了。下面附上关键代码:
CustomRedisCache.java
package com.sample.config;
import org.apache.ibatis.cache.Cache;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
public class CustomRedisCache implements Cache {
/*
* id是必须的带上的,这里的id会指定当前放入缓存的mapper的namespace
* 如这里的id就是com.sample.dao.IEmployeeDao
*/
private final String id;
private final RedisTemplate redisTemplate;
public CustomRedisCache(String id) {
// 获取redis实例
redisTemplate = ApplicationContextUtil.getBean("redisTemplate",RedisTemplate.class);
// 指定key的序列化方式
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
this.id = id;
}
@Override
public String getId() {
return id;
}
@Override
public void putObject(Object key, Object value) {
redisTemplate.opsForHash().put(id.toString(),key.toString(),value);
}
@Override
public Object getObject(Object key) {
return redisTemplate.opsForHash().get(id.toString(),key.toString());
}
@Override
public Object removeObject(Object key) {
return null;
}
@Override
public void clear() {
redisTemplate.delete(id.toString());
}
@Override
public int getSize() {
return redisTemplate.opsForHash().size(id.toString()).intValue();
}
}
application.yml
server:
port: 8081
# 数据库基本配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.31.100:3306/mybatis?useSSL=false&serverTimezone=UTC
username: root
password: xxxxxx
type: com.alibaba.druid.pool.DruidDataSource
redis:
port: 6378
host: 192.168.31.100
database: 0
# mybatis相关配置
mybatis:
configuration:
map-underscore-to-camel-case: true
cache-enabled: true
type-aliases-package: com.sample.entity
mapper-locations: classpath:mapper/*.xml
# 日志配置
logging:
level:
com.sample.dao: debug