编译软件:IntelliJ IDEA 2019.2.4 x64
操作系统:win10 x64 位 家庭版
Maven版本:apache-maven-3.6.3
Mybatis版本:3.5.6
一. 缓存是什么?
一说到缓存,我们可能都会想到Cashe,这里摘自百度百科对它的解释:它原本是指访问速度比一般随机存取存储器(RAM)快的一种高速存储器,通常它不像系统主存那样使用DRAM技术,而使用昂贵但较快速的SRAM技术。缓存的设置是所有现代计算机系统发挥高性能的重要因素之一。它的工作原理是当CPU要读取一个数据时,首先从CPU缓存中查找,找到就立即读取并送给CPU处理;没有找到,就从速率相对较慢的内存中读取并送给CPU处理,同时把这个数据所在的数据块调入缓存中,可以使得以后对整块数据的读取都从缓存中进行,不必再调用内存。
以上部分内容可能看不懂,没关系,你只需要知道正是由于这样的读取机制,使得CPU读取缓存的命中率非常高(大多数CPU可达90%左右),也就是说CPU下一次要读取的数据90%都在CPU缓存中,只有大约10%需要从内存读取。这样大大节省了CPU直接读取内存的时间,非常快捷!!!
简而言之,以cpu中的缓存举例,缓存其实就是数据交换的缓冲区【又称Cashe】,是存贮数据(使用频繁的数据)的临时地方。当用户查询数据,首先在缓存中寻找,如果找到了则直接执行。如果找不到,则去数据库中查找。
二. 为什么要使用缓存?
举个生活中的例子,当我们在线观看视频时,以哔哩哔哩网站为例,你会发现,底下的进度条会实时显示蓝色,白色等两种颜色。不难发现,蓝色代表的是视频实际播放的进度,而白色代表的是视频实时预先缓存的进度。如下所示。
这种让用户一边下载一边观看、收听,而不要等整个文件下载到自己的计算机上才可以观看的网络传输技术,就是鼎鼎大名的流媒体技术。该技术的原理是先在使用者端的计算机上创建一个缓冲区,在播放前预先下一段数据作为缓冲,在网路实际连线速度小于播放所耗的速度时,播放程序就会取用一小段缓冲区内的数据,这样可以避免播放的中断,也使得播放品质得以保证。该技术在很多音频影视网站上被大量使用,旨在丰富用户的使用体验并提高音频的播放性能。
而程序中的缓存【Mybatis缓存】,亦是如此,Mybatis使用缓存优势可以提高查询效率,并降低服务器的压力。它的本质就是用利用空间换时间,牺牲数据的实时性,以服务器内存中的数据暂时代替从数据库读取最新的数据,减少数据库IO,减轻服务器压力,减少网络延迟,加快页面打开速度。
三. Mybatis中的缓存分哪几种?
👉分类
- 一级缓存
- 二级缓存
- 第三方缓存
3.1 Mybatis缓存机制之一级缓存
👉概述
一级缓存【本地缓存(Local Cache)或SqlSessiona级别缓存】
🤔什么是SqlSessiona?
SqlSession是一个会话,相当于JDBC中的一个Connection对象,是整个Mybatis运行的核心,它是MyBatis的关键对象,是执行持久化操作的独享,类似于JDBC中的Connection。它是应用程序与持久层之间执行交互操作的一个单线程对象,也是MyBatis的核心接口之一
👉特点
- 一级缓存默认开启
- 不能关闭
- 可以清空
💡 :有点类似于使用腾讯视频网站去看电影,电影观看进度条前的那一小段灰色的进度条
👉缓存原理
第一次获取数据时,先从数据库中加载数据,将数据缓存至Mybatis一级缓存中【缓存底层实现原理Map,key:hashCode+查询的Sqlld+编写的sal查询语句+参数】
以后再次获取数据时,先从一级缓存中获取,如未获取到数据,再从数据库中获取数据
不信?请看如下测试代码的体现
代码示例如下:
@Test public void test04(){ try { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //通过SqlSessionFactory对象调用openSession(); SqlSession sqlSession = sqlSessionFactory.openSession(); //获取EmployeeMapper的代理对象 EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class); //第一次调用selectEmpByOneOpr(1); List<Employee> employees = employeeMapper.selectEmpByOneOpr(1); System.out.println(employees); System.out.println("---------------------------------------------"); //第二次调用selectEmpByOneOpr(1); List<Employee> employees1 = employeeMapper.selectEmpByOneOpr(1); System.out.println(employees1); } catch (IOException e) { e.printStackTrace(); } }
👉一级缓存的五种失效情况
不同的SqlSession对应不同的一级缓存
同一个SqlSession但是查间条件不同
同一个SqlSession两次查询期间执行了任何一次增删改操作
执行任何一次的增删改操作会默认清空一级缓存同一个SqlSession两次查询期间手动清空了缓存
如何手动清空一级缓存?
sqlSession.clearCache()
同一个SqlSessioni两次查询期间提交了事务
sqlSession.commit()
3.2 Mybatis缓存机制之二级缓存
👉概述
二级缓存【全局作用域缓存】是SqlSessionFactory级别的缓存
👉特点
- 二级缓存默认关闭,需要开启才能使用
- 二级缓存需要提交sqlSession或关闭sqlSessionl时,才会缓存。
👉二级缓存使用的步骤
①全局配置文件中开启二级缓存“<setting name="cacheEnab1ed"value=“true”/>”
②需要使用二级缓存的映射文件处使用cache配置缓存
③注意:POJO(Java Bean【java的实体类】)需要实现Serializable接口
④关闭sqlSession或提交sqlSessionl时,将数据缓存到二级缓存
👉用法案例
演示二级缓存的效果
代码示例如下:
①全局配置文件中开启二级缓存
②这里假设映射文件EmployeeMapper.xml使用cache配置缓存
③在映射文件EmployeeMapper.xml对应的pojo类实现Serializable接口
④测试
@Test public void test04(){ try { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //通过SqlSessionFactory对象调用openSession()获取sqlSession对象; SqlSession sqlSession = sqlSessionFactory.openSession(); //通过sqlSession对象调用getMapper(EmployeeMapper.class)以获取EmployeeMapper的代理对象 EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class); //第一次调用selectEmpByOneOpr(1); List<Employee> employees = employeeMapper.selectEmpByOneOpr(1); System.out.println(employees); //关闭sqlsession,目的是为了将数据加载到二级缓存中 sqlSession.close(); System.out.println("---------------------------------------------"); //重新获取sqlsession对象 SqlSession sqlSession1 =sqlSessionFactory.openSession(); //使用sqlsession对象调用etMapper(EmployeeMapper.class)获取EmployeeMapper接口的代理对象mapper EmployeeMapper mapper = sqlSession1.getMapper(EmployeeMapper.class); //调用selectEmpByOneOpr(1); List<Employee> employees1 = mapper.selectEmpByOneOpr(1); System.out.println(employees1); } catch (IOException e) { e.printStackTrace(); } }
👉底层原理
第一次获取数据时,先从数据库中获取数据,将数据缓存至一级缓存;当提交或关闭SqlSessionl时,将数据缓存至二级缓存
以后再次获取数据时,先从一级缓存中获取数据,如果一级缓存没有指定数据,再去二级缓存中获取数据。如果二级缓存也没有指定数据时,需要去数据库中获取数据
👉二级缓存相关属性(在设置了cash的映射文件中设置以下属性)
eviction=“FIFO"
:缓存清除【回收】策略
LU-最近最少使用的
:移除最长时间不被使用的对象FFO-先进先出
:按对象进入缓存的顺序来移除它们
flushlnterval
:刷新间隔,单位毫秒,默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新size
:引用数目,正整数,代表缓存最多可以存储多少个对象,太大容易导致内存益出readOnly
:只读,true/false
true
:只读缓存;会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势false
:读写缓存;会返回缓存对象的拷贝(通过序列化)。这会慢一些,但是安全,因此默认是false
👉二级缓存的失效情况
①在两次查询之间,执行增删改操作,会同时清空一级缓存和二级缓存
②sqlSession.clearCache()只是用来清除一级缓存
🤔思考
执行两次查询操作(查询条件相同且查询语句相同),中间使用sqlsession.clearCache(),然后关闭第一个sqlsession对象,又新建一个sqlsession对象,执行第二次查询,是否会导致二级缓存失效?
我的推测: 会失效
测试代码如下:
@Test public void test05() { try { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //通过SqlSessionFactory对象调用openSession()获取sqlSession对象; SqlSession sqlSession = sqlSessionFactory.openSession(); //通过sqlSession对象调用getMapper(EmployeeMapper.class)以获取EmployeeMapper的代理对象 EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class); //第一次调用selectEmpByOneOpr(1); List<Employee> employees = employeeMapper.selectEmpByOneOpr(1); System.out.println(employees); //清空一级缓存 sqlSession.clearCache(); //需要先关闭sqlsession对象,以缓存数据到二级缓存 sqlSession.close(); System.out.println("---------------------------------------------"); //重新获取sqlsession对象 SqlSession sqlSession1 = sqlSessionFactory.openSession(); //使用sqlsession对象调用etMapper(EmployeeMapper.class)获取EmployeeMapper接口的代理对象mapper EmployeeMapper mapper = sqlSession1.getMapper(EmployeeMapper.class); /* //在两个查询之间执行一次更新操作,目的是为了清空二级缓存 Employee e = new Employee(); e.setId(1); e.setSalary(25000.0); mapper.updateEmp(e);*/ //调用selectEmpByOneOpr(1); List<Employee> employees1 = mapper.selectEmpByOneOpr(1); System.out.println(employees1); } catch (IOException e) { e.printStackTrace(); } }
推测错了,二级缓存仍然可以命中该查询结果
⭐原因分析
当第一次执行某个 SQL 语句时,该 SQL 语句的缓存条目会被添加到二级缓存中;而在清空当前 SqlSession 对象的一级缓存时,并没有清空对应的 SQL 语句在二级缓存中的缓存条目,因此该 SQL 语句的缓存条目仍然存在于二级缓存中,即使这个二级缓存还没有和数据库同步。
当新建另一个 SqlSession 对象,执行相同的查询操作且查询条件和前一次查询操作相同时,MyBatis 将会先从一级缓存中尝试获取数据。由于已经执行了 sqlsession.clearCache()
清空了当前 SqlSession 的一级缓存,因此一级缓存会命中失败,但是 MyBatis 可以从二级缓存中获取到之前查询过的结果集,返回给我当前的查询操作结果。
3.3 Mybatis中缓存机制之第三方缓存【以EhCache为例】
👉概述
EhCache【第三方缓存】是一个纯ava的进程内缓存框架
👉使用步骤
①导入jar包
代码示例如下:
<!-- 导入ehcache的jar包 --> <dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-ehcache</artifactId> <version>1.0.3</version> </dependency> <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api --> <!-- 导入一个日志的jar包 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.6.2</version> </dependency>
②编写ehcache的配置文件(resources目录下)【ehcache.xml】
<?xml version="1.0" encoding="UTF-8"?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../config/ehcache.xsd"> <!-- 磁盘保存路径 --> <diskStore path="E:\mybatis\ehcache" /> <defaultCache maxElementsInMemory="512" maxElementsOnDisk="10000000" eternal="false" overflowToDisk="true" timeToIdleSeconds="120" timeToLiveSeconds="120" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"> </defaultCache> </ehcache>
③加载第三方缓存【映射文件】
<!-- 此处加载第三方缓存ehcache --> <cache type="org.mybatis.caches.ehcache.EhcacheCache"></cache>
④开始使用
@Test public void test04(){ try { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //通过SqlSessionFactory对象调用openSession()获取sqlSession对象; SqlSession sqlSession = sqlSessionFactory.openSession(); //通过sqlSession对象调用getMapper(EmployeeMapper.class)以获取EmployeeMapper的代理对象 EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class); //第一次调用selectEmpByOneOpr(1); List<Employee> employees = employeeMapper.selectEmpByOneOpr(1); System.out.println(employees); //关闭sqlsession,目的是为了将数据加载到二级缓存中 sqlSession.close(); System.out.println("---------------------------------------------"); //重新获取sqlsession对象 SqlSession sqlSession1 =sqlSessionFactory.openSession(); //使用sqlsession对象调用etMapper(EmployeeMapper.class)获取EmployeeMapper接口的代理对象mapper EmployeeMapper mapper = sqlSession1.getMapper(EmployeeMapper.class); //调用selectEmpByOneOpr(1); List<Employee> employees1 = mapper.selectEmpByOneOpr(1); System.out.println(employees1); } catch (IOException e) { e.printStackTrace(); } }
❗注意
①第三方缓存,需要建立在二级缓存基础上【需要开启二级缓存,第三方缓存才能生效】
②如何让第三方缓存失效?
将二级缓存设置失效即可【在两次查询之间,进行一次增删改操作以清除二级缓存】
代码示例如下:
@Test public void test04(){ try { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); //通过SqlSessionFactory对象调用openSession()获取sqlSession对象; SqlSession sqlSession = sqlSessionFactory.openSession(); //通过sqlSession对象调用getMapper(EmployeeMapper.class)以获取EmployeeMapper的代理对象 EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class); //第一次调用selectEmpByOneOpr(1); List<Employee> employees = employeeMapper.selectEmpByOneOpr(1); System.out.println(employees); //关闭sqlsession,目的是为了将数据加载到二级缓存中 sqlSession.close(); System.out.println("---------------------------------------------"); //重新获取sqlsession对象 SqlSession sqlSession1 =sqlSessionFactory.openSession(); //使用sqlsession对象调用etMapper(EmployeeMapper.class)获取EmployeeMapper接口的代理对象mapper EmployeeMapper mapper = sqlSession1.getMapper(EmployeeMapper.class); //在两个查询操作之间执行一次更新操作,目的是为了清空二级缓存 Employee e=new Employee(); e.setId(1); e.setSalary(25000.0); mapper.updateEmp(e); //调用selectEmpByOneOpr(1); List<Employee> employees1 = mapper.selectEmpByOneOpr(1); System.out.println(employees1); } catch (IOException e) { e.printStackTrace(); } }