MyBatis缓存机制(一级缓存,二级缓存)

279 篇文章 1 订阅
162 篇文章 1 订阅

写在前面:2020年面试必备的Java后端进阶面试题总结了一份复习指南在Github上,内容详细,图文并茂,有需要学习的朋友可以Star一下!
GitHub地址:https://github.com/abel-max/Java-Study-Note/
一,MyBatis一级缓存(本地缓存)
My Batis 一级缓存存在于 SqlSession 的生命周期中,是SqlSession级别的缓存。在操作数据库时需要构造SqlSession对象,在对象中有一个数据结构用来存储缓存数据。不同的SqlSession之间的数据缓存是不能共享的。

在同一个SqlSession 中查询数据时,sqlSession会先在一级缓存中查找,如果有,直接读取,如果没有,则从数据库中查询, 接着把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一级缓存中(以Map对象的形式)。如果后面再次执行相同方法,SqlSession通过算法会生成相同的键值,然后在一级缓存中查找,由于一级缓存中己经存在该键值,所以会返回缓存中的对象。与执行select不同的是,执行update,insert,delect操作后会清空一级缓存中的数据,而不是通过算法生成缓存的键值存入一级缓存,之所以有这种差别是因为 select的flushCache(清空缓存)默认为false,而update,insert,delect的flushCache(清空缓存)默认为true。

当然也可以使用下面的方法对select操作进行设置,

select * from student where sid=#{Sid} and s_name=#{Sname} 就是在原来方法的基础上增加了 flushCache= true ,这个属性配置为 true 后,在查询数据后会清空当前的一级缓存,因此调用该方法后每次都会重新从数据库中查询数据,但是由于这个方法清空了一级缓存,会影响当前 SqlSession 中所有缓存的查询,因此在需要反复查询获取只读数据的情况下,会增加数据库的查询次数,所以要避免这么使用。

除了上面讲的将 flushCache赋值为true的情况外,还会导致一级缓存清空的情况就是关闭第一个 SqlSession,然后重新开启一个SqlSession,由于一级缓存是和 SqlSession 绑定的,只存在于 SqlSession的生命周期中,所以在新的SqlSession中调用刚才的方法,在缓存中就查不到,必须去数据库中查询,当然之后在调用过该方法并不清除的情况下就可以在缓存中取到了。

一级缓存原理图:
在这里插入图片描述
代码:

public class MyBatisTest {
    public static void main( String[] args ) {
        SqlSession openSession = null;
        try {
            //mybatis配置文件
            String resourse="mybatis-cfg.xml";
            //通过 Resources 工具类将 ti -config.xm 配置文件读入 Reader
            InputStream inputStream=Resources.getResourceAsStream(resourse);
            //通过 SqlSessionFactoryBuilder 建造类使用 Reader 创建 SqlSessionFactory工厂对象
            SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);
            //通过SqlSessionFactory工厂得到SqlSession
            openSession = sqlSessionFactory.openSession();
            //通过反射机制来获取对应的Mapper实例
            StudentMapper mapper=openSession.getMapper(StudentMapper.class);
            Student student1=mapper.selectStudentByIdAndName(2,"danghh");
            Student student2=mapper.selectStudentByIdAndName(2,"danghh");
            System.out.println(student1);
            openSession.commit();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //最后一定不要忘记关闭 SqlSession ,否则会因为连接没有关闭导致数据库连接数过多,造成系统崩旗
            openSession.close();
        }
    }
}

运行结果:

[DEBUG] - Setting autocommit to false on JDBC Connection[com.mysql.jdbc.JDBC4Connection@dfd3711]
[DEBUG] - ==>  Preparing: select * from student where sid=? and s_name=? 
[DEBUG] - ==> Parameters: 2(Integer), danghh(String)
[DEBUG] - <==      Total: 1
 Student{SID=2, Sname='danghh', Sage=22, Ssex='nv', course=null}
 Student{SID=2, Sname='danghh', Sage=22, Ssex='nv', course=null}

通过结果可以看出,由于代码中查询是在一个SqlSession,且两次查询过程中没有更新信息,不会导致一级缓存失效,所以结果只进行了一次数据库查询。

那如果是在两个SqlSession中分别进行查询呢?

代码:
在这里插入图片描述结果:

[DEBUG] - Opening JDBC Connection
[DEBUG] - Checked out connection 234698513 from pool.
[DEBUG] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@dfd3711]
[DEBUG] - ==>  Preparing: select * from student where sid=? and s_name=? 
[DEBUG] - ==> Parameters: 2(Integer), danghh(String)
[DEBUG] - <==      Total: 1
[DEBUG] - Opening JDBC Connection
[DEBUG] - Created connection 1836797772.
[DEBUG] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@6d7b4f4c]
[DEBUG] - ==>  Preparing: select * from student where sid=? and s_name=? 
[DEBUG] - ==> Parameters: 2(Integer), danghh(String)
[DEBUG] - <==      Total: 1
Student{SID=2, Sname='danghh', Sage=22, Ssex='nv', course=null}
Student{SID=2, Sname='danghh', Sage=22, Ssex='nv', course=null}

可以看出在使用了两个SqlSession进行查询后,在数据库进行了两次查询,也验证了SqlSession的一级缓存是和SqlSession的生命周期绑定的,作用范围也只在当前SqlSession中。

另外在演示下如果在进行查询之前进行了一次update操作会不会使一级缓存清空呢?

代码:
在这里插入图片描述运行结果:

 [DEBUG] - ==>  Preparing: select * from student where sid=? and s_name=? 
 [DEBUG] - ==> Parameters: 2(Integer), hjj(String)
 [DEBUG] - <==      Total: 1
 [DEBUG] - ==>  Preparing: update student set S_name=?,Sage=?,Ssex=? where Sid=? 
 [DEBUG] - ==> Parameters: hjj(String), 23(Integer), null, 2(Integer)
 [DEBUG] - <==    Updates: 1
 [DEBUG] - ==>  Preparing: select * from student where sid=? and s_name=? 
 [DEBUG] - ==> Parameters: 2(Integer), hjj(String)
 [DEBUG] - <==      Total: 1
  Student{SID=2, Sname='hjj', Sage=23, Ssex='null', course=null}
  Student{SID=2, Sname='hjj', Sage=23, Ssex='null', course=null}

二,MyBatis二级缓存(全局缓存)

MyBatis二级缓存非常强大,它不同于一级缓存只存在于 SqlSession 的生命周期中, 而是可以理解为存在于 SqlSessionFactory 的生命周期中 ,是Mapper(studentMapper) 级别的缓存,一个Mapper对应一个二级缓存,当Mapper中的多个SqlSession共同操作同一个方法时,多个SqlSession是可以共用二级缓存的中的数据的,所以二级缓存是跨SqlSession的。

在开启二级缓存时,查出来的数据默认先存储在一级缓存中,当有 SqlSession关闭 时,它里面一级缓存中的数据就会被存储到Mapper的二级缓存中,这样该Mapper中的其他会话执行了相同方法时,就会在二级缓存中找到匹配的数据,如果没有找到,才会去数据库中查找。注意只有在该会话关闭时,它一级缓存中的数据才会被刷到二级缓存中。另外如果只是开启二级缓存的全局(config)开关,而会话(student)没有开启二级缓存,查询时也不会在二级缓存中查询。

一级缓存( 也叫本地缓存)一般默认会启开,不需要进行配置,但要使用二级缓存就需要进行配置。 那如何配置呢?

第一步 :在全局配置文件中添加

在这里插入图片描述
(这个参数是二级缓存的全局开关,默认值是 true ,初始状态为启用状态,所以也可忽略此步的配置)

(由于MyBatis二级缓存和命名空间namespace是绑定的 ,即二级缓存还需要在 Mapper.xml 映射文件中配置或者在 Mapper.java 接口中配置。)

第二步 :在Sql映射文件中添加元素。

在这里插入图片描述
上面的配置创建了一个 FIFO 缓存,并每隔6秒刷新一次,存储集合或对象的1024个引用,而且返回的对象被认为是非只读的。

eviction :缓存的收回策略

LRU (最近最少使用的) 移除最长时间不被使用的对象,这是默认值
FIFO (先进先出〉 按对象进入缓存的顺序来移除它们
SOFT (软引用) 移除基于垃圾回收器状态和软引用规则的对象
WEAK (弱引用) 更积极地移除基于垃圾收集器状态和弱引用规则的对象
flushinterval :刷新间隔

设置缓存多长时间清空一次,单位为毫秒值,默认不清空。

readOnly:是否只读

true:只读,设置为true后,mybatis认为所有从缓存中获取数据的操作都是只读操作,不会修改数据,因此为了加快获取速度,一般会直接将数据在缓存中的引用交给用户,虽然速度快,但不安全。

false:非只读,设置为false后,mybatis认为获取的数据可能会被修改,因此会利用序列化和反序列化的技术克隆一份新的数据给你,虽然速度慢,但安全。

默认是 false

size :引用数目

设置缓存可以存放的引用数目,可以被设置为任意正整数 。默认值是 1024。

第三步 :给POJO类实现序列化接口
在这里插入图片描述
代码:

public class MyBatisTest {
    public static void main( String[] args ) {
        SqlSession openSession1 = null;
        SqlSession openSession2 = null;
        try {
            //mybatis配置文件
            String resourse="mybatis-cfg.xml";
            //通过 Resources 工具类将 ti -config.xm 配置文件读入 Reader
            InputStream inputStream=Resources.getResourceAsStream(resourse);
            //通过 SqlSessionFactoryBuilder 建造类使用 Reader 创建 SqlSessionFactory工厂对象
            SqlSessionFactory sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);
            //通过SqlSessionFactory工厂得到SqlSession1
            openSession1 = sqlSessionFactory.openSession();
            StudentMapper mapper1=openSession1.getMapper(StudentMapper.class);
            //通过SqlSessionFactory工厂得到SqlSession2
            openSession2 = sqlSessionFactory.openSession();
            StudentMapper mapper2=openSession2.getMapper(StudentMapper.class);
            //使用会话1进行查询,此次查询结果只会存储在一级缓存中
            Student student1=mapper1.selectStudentByIdAndName(2,"hjj");
            System.out.println(student1);
            //使用会话2进行查询,前面会话未关闭,数据不会被刷到二级缓存中,所以本次仍会执行sql
            Student student2=mapper2.selectStudentByIdAndName(2,"hjj");
            System.out.println(student2);
            //使用会话2进行查询,由于前面已执行过该方法,所以可在一级缓存中查到
            Student student3=mapper2.selectStudentByIdAndName(2,"hjj");
            System.out.println(student3);
            openSession1.commit();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            //最后一定不要忘记关闭 SqlSession ,否则会因为连接没有关闭导致数据库连接数过多,造成系统崩旗
            openSession1.close();
        }
    }
}

运行结果:

[DEBUG] - Cache Hit Ratio [MyBatisDemo.StudentMapper]: 0.0
[DEBUG] - Opening JDBC Connection
[DEBUG] - Checked out connection 234698513 from pool.
[DEBUG] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@dfd3711]
[DEBUG] - ==>  Preparing: select * from student where sid=? and s_name=? 
[DEBUG] - ==> Parameters: 2(Integer), hjj(String)
[DEBUG] - <==      Total: 1
     Student{SID=2, Sname='hjj', Sage=23, Ssex='null', course=null}
[DEBUG] - Cache Hit Ratio [MyBatisDemo.StudentMapper]: 0.0
[DEBUG] - Opening JDBC Connection
[DEBUG] - Created connection 1843368112.
[DEBUG] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@6ddf90b0]
[DEBUG] - ==>  Preparing: select * from student where sid=? and s_name=? 
[DEBUG] - ==> Parameters: 2(Integer), hjj(String)
[DEBUG] - <==      Total: 1
     Student{SID=2, Sname='hjj', Sage=23, Ssex='null', course=null}
[DEBUG] - Cache Hit Ratio [MyBatisDemo.StudentMapper]: 0.0
      Student{SID=2, Sname='hjj', Sage=23, Ssex='null', course=null}
[DEBUG] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@dfd3711]
[DEBUG] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@dfd3711]
[DEBUG] - Returned connection 234698513 to pool.

该颜色: 表示会话1第一次查询的结果,由于第一次查询,一级缓存和二级缓存中都没有数据,所以Mapper命中率为0.0,且进行了数据库查询,并将结果存储到会话1一级缓存中。

该颜色: 表示会话2第一次查询的结果,由于会话1没有关闭,所以会话1的一级缓存不会刷到Mapper的二级缓存中,并且是在会话2中第一次查询该方法,所以Mapper命中率为0.0,且进行了数据库查询,并将结果存储到会话2的一级缓存中。

该颜色: 表示会话2第二次查询的结果,虽然会话1没有关闭,会话1的一级缓存不会刷到Mapper的二级缓存中,但是在会话2中查询过该方法,在会话2的一级缓存中已存在该数据,所以Mapper命中率为0.0,没有进行数据库查询。

接下来就验证下会话中一级缓存的数据是不是只有在该会话关闭后才会被刷新到mapper的二级缓存!

代码:(仅截取部分)

在这里插入图片描述运行结果:

[DEBUG] - Cache Hit Ratio [MyBatisDemo.StudentMapper]: 0.0
[DEBUG] - Opening JDBC Connection
[DEBUG] - Checked out connection 234698513 from pool.
[DEBUG] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@dfd3711]
[DEBUG] - ==>  Preparing: select * from student where sid=? and s_name=? 
[DEBUG] - ==> Parameters: 2(Integer), hjj(String)
[DEBUG] - <==      Total: 1
  Student{SID=2, Sname='hjj', Sage=23, Ssex='null', course=null}
[DEBUG] - Cache Hit Ratio [MyBatisDemo.StudentMapper]: 0.0
  Student{SID=2, Sname='hjj', Sage=23, Ssex='null', course=null}
[DEBUG] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@dfd3711]
[DEBUG] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@dfd3711]
[DEBUG] - Returned connection 234698513 to pool.
[DEBUG] - Cache Hit Ratio [MyBatisDemo.StudentMapper]: 0.3333333333333333
  Student{SID=2, Sname='hjj', Sage=23, Ssex='null', course=null}
[DEBUG] - Cache Hit Ratio [MyBatisDemo.StudentMapper]: 0.5
  Student{SID=2, Sname='hjj', Sage=23, Ssex='null', course=null}

来源:https://www.tuicool.com/articles/AVnUNvn

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值