文章目录
mybatis二级缓存简介
myabtis中应用级缓存(二级缓存) SqlSessionFactory中相同的namespace才能会话共享。
开启缓存的方式非常简单,只需要在sql映射文件中加上
service
public interface IAppService {
List<App> findAll();
}
dao
@Component
public interface AppDAO {
List<App> findAll();
}
mapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cache.mycache.dao.AppDAO">
<cache/>
<!-- 查询-->
<select id="findAll" resultType="com.cache.mycache.entity.App">
select *from mnt_app order by app_id
</select>
</mapper>
测试
System.out.println("第一次");
appService.findAll();
System.out.println("第二次");
appService.findAll();
结果
总结:
mybatis自身提供的二级缓存是本地缓存,实际上是把数据缓存到了自身服务器上(tomcat)的虚拟中,所以当我们停止项目时,缓存就会清除。所以每次重启项目时,第一次都是从数据库中拿数据,这也是本地缓存的弊端。
mybatis二级缓存源码阅读
- 查看mybatis的缓存实现。
<cache/ >为我们提供了type属性,默认的属性值是PerpetualCache
相当于 < cache type=“org.apache.ibatis.cache.impl.PerpetualCache” />
cache源码
需要注意的是,我们下载源文档发现。
我们发现是没有使用的,也就是说,当我们使用mybatis自身的二级缓存时,是没有删除某个缓存的操作的,如遇到数据的增删改,是直接进行清空缓存的。
PerpetualCache
我们发现,缓存的底层其实就是一个HashMap,通过对hashmap的增删改查,来实现缓存操作。
我们通过断点调试,看一下执行流程
put操作
进行下一步。
我们发现,
key存储的是 编码+方法名称+编码+查询语句
value存储的是 数据库中查询的信息。
get操作
我们发现,get的key是我们put时,进行存储的key。
通过redis实现mybatis分布式缓存
之前我们学习redis时,我们可以通过springboot提供的reids的一些操作,将数据存储到reids中。那么我们是否可以改变一下mybatis二级缓存的默认实现,实现cache,重写方法时,用redis的操作进行重写,进而将缓存内容存入到reids中呢?
替换为redis缓存,实现cache接口
实现步骤:
1. 创建RedisCache类,实现Cache接口。
public class RedisCache implements Cache {
@Override
public String getId() {
return null;}
@Override
public void putObject(Object key, Object value) {
}
@Override
public Object getObject(Object key) {
return null;}
@Override
public Object removeObject(Object key) {
return null;}
@Override
public void clear() {
}
@Override
public int getSize() {
return 0;}
@Override
public ReadWriteLock getReadWriteLock() {
return null;}
}
2. < cache /> type指向rediscache的实现
<cache type="com.cache.mycache.cache.RedisCache"/ >
3. 测试rediscache中需要的内容。所有方法空实现直接运行测试。
Base cache implementations must have a constructor that takes a String id as a parameter
出错一:需要一个构造方法,并且以一个string类型的id作为形参。
打印id:com.cache.mycache.dao.AppDAO 我们发现id就是namespace
Caused by: java.lang.IllegalArgumentException: name argument cannot be null
出错二:getId的返回值不能为空。(即getId的返回值不能为空) id是当前的文件对应的Dao层com.cache.mycache.dao.AppDAO 即namespace
4. 测试一下缓存的执行流程。我们打印set和get里面的key和value
//缓存存入
@Override
public void putObject(Object key, Object value) {
System.out.println(key.toString());
System.out.println(value.toString());
}
//缓存取出
@Override
public Object getObject(Object key) {
System.out.println(key.toString());
return null;
}
5. 使用redisRemplate来进行redis缓存,创建获取redisTemplate的工具类
问题:我们知道RedisCache的实例化是交给sql映射文件的。实例化时,传入的id是namespace。而RedisCache并不是我们的工厂类,所以我们不能直接注入RedisTemplate。
我们可以使用ApplicationContext的getBean(String name)通过工厂来获取RedisTemplate对象。
//用来获取springboot创建好的工厂
@Configuration
public class ApplicationContextUtils implements ApplicationContextAware {
//保留下来的工厂
private static ApplicationContext applicationContextProdect;
//将创建好的工厂以参数形式传递给这个类
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
applicationContextProdect = applicationContext;
}
//提供在工厂中获取对象的方法 通过名字获取 RedisTemplate 在工厂中的对象就是redisTemplate
public static Object getBean(String beanName) {
return applicationContextProdect.getBean(beanName);
}
//getbean有两种方式拿:1)按类型拿 2)按名字拿
//从应用上下文里面获得类实例(即bean容器里面获得类容器)
//为什么我们现在要采用这种麻烦的方法(以前直接用Autowired注解自动装配进去了)--- 这与redis连接池有关
//用Redis时,建了许多连接池,我们在redis里面拿缓存对象时,缓存对象与每个连接都有一个RedisTemplate,你在注入时用自动注入,不同
// RedisTemplate是同类型同名的,注入时你得到的是哪个连接使用的redisTemplate呢?所以你注入时分不清
//所以我们重新封装一个getBean的方法,按指定类型或名字来拿bean实例
public static <T> T getBean(Class<T> tClass) {
return applicationContextProdect.getBean(tClass);
}
//或者
// public static <T> T getBean(String name) {
// return (T) applicationContextProdect.getBean(name);
// }
}
6. RedisConfig的类方法的put和get 的实现。
//缓存存入
@Override
public void putObject(Object key, Object value) {
//我们知道,id表示当前的namespace,key表示调用的方法,结果为值。相当于三个参数。此时我们可以使用Hash来存储
RedisTemplate redisTemplate = (RedisTemplate)ApplicationContextUtils.getBean("redisTemplate");
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.opsForHash().put(id,key.toString(),value);
}
//缓存取出
@Override
public Object getObject(Object key) {
RedisTemplate redisTemplate = (RedisTemplate)ApplicationContextUtils.getBean("redisTemplate");
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate.opsForHash().get(id,key.toString());
}
测试:
redis中
7. 重写清除缓存的方法
之前我们提到过,虽然redis提供了remove和clear方法,但是mybatis的缓存操作时,remove方法是不调用的。也就是说,只要 我们进行了增删改操作,mybatis默认走的是清空clear。增删改都会清空缓存
@Override
public Object removeObject(Object key) {
System.out.println("移除缓存");
return null;
}
@Override
public void clear() {
System.out.println("清空缓存");
}
测试
@Test
public void testApp(){
System.out.println("第一次查询所有");
appService.findAll();
System.out.println("第二次查询所有");
appService.findAll();
System.out.println("删除id为1的数据");
appService.deleteOne(1L);
}
因为我们用的redis缓存,所以再次重启项目时,redis缓存中有的话,就不查询的。因为redis缓存是独立项目服务器之外的,所以重启项目并不会清空redis缓存。
我们看,虽然我们删除了一条信息,但是走的是清空缓存。
8.重写获取缓存数量的方法
@Override
public int getSize() {
RedisTemplate redisTemplate = (RedisTemplate)ApplicationContextUtils.getBean("redisTemplate");
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
//返回int类型
return redisTemplate.opsForHash().size(id).intValue();
}
9.封装redisTemplate
我们在RedisCache调用就可以直接 redisTemplate.opsForHash().size(id).intValue();
//获取redisTemplate //每个连接池的连接都要获得RedisTemplate
private RedisTemplate getRedisTemplate() {
RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
10.RedisCache
//自定义Redis缓存实现
public class RedisCache implements Cache {
//当前放入缓存的namespace
private final String id;
public RedisCache(String id) {
this.id = id;
System.out.println(id);
}
@Override
public String getId() {
return this.id;
}
//缓存存入
@Override
public void putObject(Object key, Object value) {
//我们知道,id表示当前的namespace,key表示调用的方法,结果为值。相当于三个参数。此时我们可以使用Hash来存储
getRedisTemplate().opsForHash().put(id, key.toString(), value);
}
//缓存取出
@Override
public Object getObject(Object key) {
return getRedisTemplate().opsForHash().get(id, key.toString());
}
//redis的保存方法,默认不会调用 后续版本可能会调用
@Override
public Object removeObject(Object key) {
System.out.println("移除缓存");
return null;
}
@Override
public void clear() {
System.out.println("清空缓存");
getRedisTemplate().delete(id);
}
@Override
public int getSize() {
//返回int类型
return getRedisTemplate().opsForHash().size(id).intValue();
}
@Override
public ReadWriteLock getReadWriteLock() {
return null;
}
//获取redisTemplate //每个连接池的连接都要获得RedisTemplate
private RedisTemplate getRedisTemplate() {
RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
关联关系的情况分析
实际项目中,我们通常会进行联表查询。比如我要查一个用户的所在部门,此时就是一个用户表,还有一个部门表。
查询示例:
<select id="findUserAndDeptById" parameterType="com.cache.mycache.entity.dto.UserDTO" resultMap="userMessage">
select u.nick_name,d.name from sys_user u,sys_dept d where u.dept_id=d.id and u.id=#{
id}
</select>
<resultMap id="userMessage" type="com.cache.mycache.entity.dto.UserDTO">
<association property="dept" javaType="com.cache.mycache.entity.Dept">
<result property="name" column="name"/>
</association>
</resultMap>
@Test
public void testAll(){
//查询一条用户信息
userService.findOne(1L);
//查询一条部门信息
deptService.findOne(1L);
//查询一条用户和部门信息
userService.findUserAndDeptById(1L);
}
缓存中:
我们发现,缓存中有两条数据,其中基于com.cache.mycache.dao.UserDao的有两条。包含一条用户信息和用户及其部门信息。
删除示例
1.删除一条部门信息(缓存中默认有上面的三条记录)
@Test
public void testAll(){
userService.findOne(1L);
deptService.findOne(1L);
userService.findUserAndDeptById(1L);
deptService.deleteOne(1L);
}
此时缓存:
我们发现,此时com.cache.mycache.dao.DeptDAO下面的数据已经删除了。
此时问题来了,com.cache.mycache.dao.UserDao中存储了一条关于用户及其部门的记录,并没有删除,那么当我们再次执行查询时,返回的并不是数据库中真实的数据,而是缓存中的假数据。
解决方案
mybatis为我们提供了cache-ref这个缓存标签,通过他,我们可以将两个或多个namespace绑定在一起,当其中一个发生增删改时,就清空相关联的所有namespace的内容。
比如,我使用com.cache.mycache.dao.UserDAO关联com.cache.mycache.dao.DeptDAO,当其中一个namespace发生增删改时,会把这两个namespace下缓存的所有记录清空。
存储缓存数据时,谁关联了谁,数据就缓存到主动关联的那个namespace(即com.cache.mycache.dao.UserDAO).
我们发现,com.cache.mycache.dao.UserDAO并没有设置缓存方式,他会自动使用com.cache.mycache.dao.DeptDAO的关联方式。
删除测试
@Test
public void testAll(){
userService.findOne(1L);
deptService.findOne(1L);
userService.findUserAndDeptById(1L);
deptService.deleteOne(1L);
}
缓存:
我们发现,当我们删除dept时,与他相关联的用户也删除了。说明关联起了作用。同理,当删除用户的一条信息时,dept的缓存也会随之清空。
注意:
因为user关联的dept,所以user的缓存存储方式(redis),以及存储位置(key,namespace)都是基于dept的。我们这次不进行删除,查看一下缓存的存储位置
@Test
public void testAll(){
userService.findOne(1L);
deptService.findOne(1L);
userService.findUserAndDeptById(1L);
}
我们发现,二者的缓存都存储在了dept的namespace下面。
mybatis二级缓存优缺点分析
优点
- 实现简单,使用方便。只需要一个cache注解即可。
- 维护简单,只要进行增删改,就全部删除相关缓存数据,不需要考虑脏数据。
缺点
- 灵活性差,死板。不能移除某一个缓存,只要发生增删改,就清除所有的缓存。
- 使用太过局限,在增删改比较多的系统,但同时数据量比较大的系统时,频繁的清空缓存,并不利于性能的提升。
缓存穿透
客户端查一个数据库中没有的数据。某些木马程序,大量请求数据库中没有的程序时,导致系统崩溃。(id=-1,id=random等。一直请求)
解决方案:将没有查询到的数据进行缓存,value设置为null。(不同担心日后添加了该key的值,缓存里面没有。因为我们进行增删改操作,缓存都会进行清空处理的)
public void testAll(){
userService.findOne(-1L);
deptService.findOne(-2L);
userService.findUserAndDeptById(-3L);
}
没有的数据,myabtis自动缓存了空值。避免恶意的数据库请求。
缓存雪崩
某一时刻,所有缓存失效。同时客户端进行大量请求。
解决方案: 1.缓存永久存储(即不设置超时时间。不推荐) 2.不同模块设置不同的缓存超时时间。
附:redis数据乱码解决方法
之前我们的演示,存储在redis中的value都是乱码的。是因为我们默认使用的是jdk的序列化方式。我们使用FastJson的序列化方式即可解决这个问题。
使用方式
1.引入依赖
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
- 设置redisTemplate的序列化方式
//获取redisTemplate //每个连接池的连接都要获得RedisTemplate
private RedisTemplate getRedisTemplate() {
RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
redisTemplate.setValueSerializer(fastJsonRedisSerializer);
redisTemplate.setHashValueSerializer(fastJsonRedisSerializer);
return redisTemplate;
}
3.效果展示