myabtis二级缓存整合Redis

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二级缓存源码阅读

  1. 查看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二级缓存优缺点分析

优点

  1. 实现简单,使用方便。只需要一个cache注解即可。
  2. 维护简单,只要进行增删改,就全部删除相关缓存数据,不需要考虑脏数据。

缺点

  1. 灵活性差,死板。不能移除某一个缓存,只要发生增删改,就清除所有的缓存。
  2. 使用太过局限,在增删改比较多的系统,但同时数据量比较大的系统时,频繁的清空缓存,并不利于性能的提升。

缓存穿透

客户端查一个数据库中没有的数据。某些木马程序,大量请求数据库中没有的程序时,导致系统崩溃。(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>
  1. 设置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.效果展示
在这里插入图片描述

参考文章

用Redis做Mybatis二级缓存

猜你喜欢

转载自blog.csdn.net/zhang19903848257/article/details/115181291