文章目录
MyBatis的缓存
1、缓存概述
-
什么是缓存?
缓存:存储在计算机上的一个原始数据复制集,就是数据交换的缓冲区(称作Cache),是存贮数据(使用频繁的数据)的临时地方。当用户查询数据,首先在缓存中寻找,如果找到了则直接执行。如果找不到,则去数据库中查找。
-
缓存有什么用?
缓存的本质就是用空间换时间,牺牲数据的实时性,以服务器内存中的数据暂时代替从数据库读取最新的数据,减少数据库IO,减轻服务器压力,减少网络延迟,加快页面打开速度。
-
缓存的分类:
- 文件缓存:文件缓存是把一些需要高速存取的变量缓存在内存中。模板引擎用的就是文件缓存机制,把动态代码编译成静态文件放入硬盘,不用每次访问都编译,直接读出即可。
- 浏览器缓存:浏览器缓存根据一套与服务器约定的规则进行工作,在同一个会话过程中会检查一次并确定缓存的副本足够新。如果在浏览过程中前进或后退时访问到同一个图片,这些图片可以从浏览器缓存中调出而即时显示。
- 数据库缓存:常用的缓存方案有memcached、redis等。把经常需要从数据库查询的数据、或经常更新的数据放入到缓存中,这样下次查询时,直接从缓存直接返回,减轻数据库压力,提升数据库性能。
- Web应用层缓存:应用层缓存指的是从代码层面上,通过代码逻辑和缓存策略,实现对数据、页面、图片等资源的缓存,可以根据实际情况选择将数据存在文件系统或者内存中,减少数据库查询或者读写瓶颈,提高响应效率。
- 服务器缓存:包括代理服务器缓存和CDN缓存。
- 代理服务器缓存:代理服务器是浏览器和源服务器之间的中间服务器,浏览器先向这个中间服务器发起Web请求,经过处理后(比如权限验证,缓存匹配等),再将请求转发到源服务器。
- CDN缓存:也叫网关缓存、反向代理缓存。CDN缓存一般是由网站管理员自己部署,为了让他们的网站更容易扩展并获得更好的性能。
MyBatis的缓存属于Web应用层缓存,在MyBatis中缓存被分为两大类:系统缓存和自定义缓存
- 系统缓存:是MyBatis中自带的缓存,这些缓存都是MyBatis帮我们设计好了的,直接就能用,主要包含一级缓存和二级缓存
- 自定义缓存:顾名思义,就是自己定义的缓存,需要自己手动去设计,当然一般而言我们并不会真正的区设计一个缓存,而是直接使用第三方设计的缓存,比如:Redis、Enhance等
2、MyBatis的一级缓存
一级缓存:一级缓存是默认开启的,它的范围是
SqlSession
级别的,当我们用SqlSession来查询数据的时候,如果下一次再使用相同的SqlSession进行查询的时候,就会直接从缓存中取数据,如果没有才从数据库中取数据,缓存只针对查询功能有效
2.1 一级缓存的使用
示例:
import com.hhxy.mapper.CacheMapper;
import com.hhxy.pojo.Emp;
import com.hhxy.utils.SqlSessionFactoryUtil;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.junit.Test;
/**
* @author ghp
* @date 2022/9/9
*/
public class CacheMapperTest {
@Test
public void cacheMapperTest(){
//1、获取SqlSessionFactory对象
SqlSessionFactory sqlSF = SqlSessionFactoryUtil.getSqlSF();
//2、获取SqlSession对象
SqlSession sqlS1 = sqlSF.openSession();
SqlSession sqlS2 = sqlSF.openSession();
//3、获取Mapper接口对象
CacheMapper mapper1 = sqlS1.getMapper(CacheMapper.class);
CacheMapper mapper2 = sqlS2.getMapper(CacheMapper.class);
//4、执行SQL
int empId = 20;
/*使用SqlSession1执行SQL*/
Emp emp1 = mapper1.getEmpById(empId);
System.out.println("emp1 = " + emp1);
Emp emp2 = mapper1.getEmpById(empId);
System.out.println("emp2 = " + emp2);
/*使用SqlSession2执行SQL*/
Emp emp3 = mapper2.getEmpById(empId);
System.out.println("emp3 = " + emp3);
//5、释放资源
}
}
2.2 一级缓存的失效
使一级缓存失效的四种情况:
-
不同的SqlSession对应不同的一级缓存
这是由一级缓存的级别决定的,详情见上面的示例,不在赘述
-
同一个SqlSession但是查询条件不同
这个也很好理解,不同条件查询得到的数据是不一致的
-
同一个SqlSession两次查询期间执行了任何一次增删改操作
因为增、删、改会清空一级缓存,原因:缓存中的数据毕竟不是数据库中的即时数据,在执行增删改操作后,可能会影响到我们上一次刚查询的数据(此时缓存中的数据就已经过时了,比如:进行第一次查询,数据进入缓存,但进行一个删除操作后将这条进入缓存的数据从数据库中删除了,第二次查询这条数据已经不存在了,直接使用缓存中的数据显然的错误的),所以MyBatis干脆就规定了增删改后,直接清空一级缓存,这样就能保证二次查询的正确性
-
同一个SqlSession两次查询期间手动清空了缓存
//清空一级缓存的代码 sqlSession.clearCache();
3、MyBatis的二级缓存
二级缓存:二级缓存需要手动开启,它的范围是
SqlSessionFactory
级别的,通过同一个SqlSessionFactory创建的SqlSession查询的结果会被缓存;此后若再次执行相同的查询语句,结果就会从缓存中获取
3.1 二级缓存的开启
-
二级缓存的开启条件:
-
在核心配置文件中,设置全局配置属性
cacheEnabled="true"
,默认为true,不需要设置<setting name="cacheEnabled" value="true"/>
-
在映射文件中设置标签
<cache/>
-
二级缓存必须在SqlSession关闭或提交之后有效,不进行关闭或提交,则查询的数据会自动进入一级缓存
-
查询的数据所转换的实体类类型必须实现序列化的接口,否则报
java.io.NotSerializableException: com.hhxy.pojo.Emp
异常
-
示例:
二级缓存测试代码:
import com.hhxy.mapper.CacheMapper;
import com.hhxy.pojo.Emp;
import com.hhxy.utils.SqlSessionFactoryUtil;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.junit.Test;
/**
* @author ghp
* @date 2022/9/9
*/
public class CacheMapperTest {
@Test
public void cacheMapperTest(){
//1、获取SqlSessionFactory对象
SqlSessionFactory sqlSF = SqlSessionFactoryUtil.getSqlSF();
//2、获取SqlSession对象
SqlSession sqlS1 = sqlSF.openSession();
SqlSession sqlS2 = sqlSF.openSession();
//3、获取Mapper接口对象
CacheMapper mapper1 = sqlS1.getMapper(CacheMapper.class);
CacheMapper mapper2 = sqlS2.getMapper(CacheMapper.class);
//4、执行SQL
int empId = 20;
/*使用SqlSession1执行SQL*/
Emp emp1 = mapper1.getEmpById(empId);
System.out.println("emp1 = " + emp1);
sqlS1.close();//如果去掉这句话,就会执行两遍SQL
/*使用SqlSession2执行SQL*/
Emp emp2 = mapper2.getEmpById(empId);
System.out.println("emp3 = " + emp2);
//5、释放资源
}
}
备注:只有二级缓存才会输出缓存命中率
3.2 二级缓存的失效
-
使二级缓存失效的情况: 两次查询之间执行了任意的增、删、改,会使一级和二级缓存同时失效
因为增、删、改清除了二级缓存,当第二次进行查询时,先到缓存区中寻找是否具有所需数据,结果由于缓存区被清空了,所以重新执行SQL语句到数据库中查询并获取数据
3.2 二级缓存相关配置
-
在mapper配置文件中添加的
cache标签
可以设置一些属性:eviction
属性:缓存回收策略,默认的是 LRU- LRU(Least Recently Used):最近最少使用的,移除最长时间不被使用的对象
- FIFO(First in First out):先进先出,按对象进入缓存的顺序来移除它们
- SOFT :软引用1,内存不足时回收,JVM会根据当前堆的使用情况来判断何时回收(一般当缓存溢出时,就会回收软引用的对象)
- WEAKReference:弱引用,GC触发就回收,JVM一旦发现使用弱引用规则指向的对象就会直接回收
- StrongReference:强引用,不会被回收,JVM宁愿抛出异常也不会回收使用强引用规则指向的对象(这是Java中最普遍的引用)
- PhantomReference:虚引用,虚引用主要用来跟踪对象的回收,并不影响对象的生命周期,使用虚引用规则指向的对象可能随时被回收
推荐阅读:
-
页面置换算法相关概念和计算:详细解说LRU算法和FIFO算法
-
flushInterval
属性:刷新间隔,单位为毫秒。默认是不设置(也就是没有刷新间隔),只有缓存被调用时进行刷新 -
size
属性:引用数目,为正整数。代表缓存最多可以存储多少个对象(太大容易导致内存溢出,太小缓存没什么意义,所以一般使用默认的) -
readOnly
属性:只读,取值为boolean类型,默认是false-
true:只读缓存,会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。
因为返回缓存对象的相同实例,所以才会将他设置成只读,因为返回缓存对象的相同实例时,如果调用者对这个对象进行修改,会导致直接修改掉缓存中的对象,存在安全隐患(只读就能解决这种安全隐患);性能优势是相较于取值时false的状态而言的,
因为读写缓存,是返回一个缓存对象的拷贝(准确来说是深拷贝),拷贝是需要耗费时间、以及资源的
-
false:读写缓存,会返回缓存对象的拷贝(通过序列化)。这会慢一些,但是安全,因此默认是 false。
这种方式能进行读写缓存的原因,是因为返回的对象是缓存对象的拷贝,当我们对缓存对象进行修改时,由于是深拷贝,返回的缓存对象的修改并不会影响到缓存中的对象,所以更安全,但是相较于只读缓存性能更低
-
4、系统缓存的查询顺序
二级缓存 → 一级缓存 二级缓存 \to 一级缓存 二级缓存→一级缓存
-
先查询二级缓存
原因:二级缓存的范围更大,二级缓存是SqlSessionFactory级别的,而一级缓存是SqlSession级别的,二级缓存中可能会有其他程序已经查出来的数据,可以拿来直接使用
-
如果二级缓存没有命中,再查询一级缓存
原因:一级缓存有二级缓存中没有的数据,二级缓存虽然范围比一级缓存大,但并不包含所有的一级缓存,二级缓存必须使用
close()
方法后才能将数据读取到二级缓存中,有一些没有使用close()
方法的查询结果只在一级缓存中 -
如果一级缓存也没有命中,则查询数据库 SqlSession关闭之后,一级缓存中的数据会写入二级缓
5、EHCache的使用
5.1 EHCache基本介绍
-
EHCache是什么?
EhCache 是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate2中默认的CacheProvider3。
Ehcache是一种广泛使用的开源Java分布式缓存。主要面向通用缓存,Java EE和轻量级容器。它具有内存和磁盘存储,缓存加载器,缓存扩展,缓存异常处理程序,一个gzip缓存servlet过滤器,支持REST和SOAP api等特点。
EHCache官网:EHCache
Hibernate介绍:Hibernate 中文文档 | Hibernate 中文网
-
EHCache的作用?
EHCache是SqlSessionFactory级别,提供二级缓存的功能,相较MyBatis自带的二级缓存具有更加优秀的性能。
-
EHCache的特点:
- 使用起来快速、简单、灵活。因为是框架嘛,当然具有框架的一般特点,使用简单,灵活性是EHCache具有多种缓存策略,同时缓存数据有两级:内存和磁盘,因此无需担心容量问题
- 健壮性、扩展性。
- 分布式。可以通过RMI、可插入API等方式进行分布式缓存
- 支持多缓存管理器实例,以及一个实例的多个缓存区域
……
PS:这些所谓的特点只需要了解即可,重点还是会使用,当你能够熟练使用多个缓存时,你就能自然而然地明白它的特点了
5.2 EHCache的基本使用
-
Step1:创建Maven工程
目录结构:
-
Step2:导入依赖
EHCache所需依赖:
<!--mysql驱动--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.16</version> </dependency> <!--mybatis--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.10</version> </dependency> <!-- Mybatis EHCache整合包 --> <dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-ehcache</artifactId> <version>1.2.2</version> </dependency> <!-- slf4j日志门面的一个具体实现 --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency>
备注:日志门面就是日志提供的接口(这里说的就是
slf4j-api
),即要使用EHCache必须先实现slf4j-api(slf4j日志门面),但是EHCache却没有实现slf4j-api,所以需要我们导入第三方slf4j-api的实现,也就是logback-classic
这个可以在Maven中看到,EHCache中是有slf4j的:
推荐阅读:浅谈日志门面
-
Step3:编写MyBatis配置文件
略……
推荐阅读:快速上手MyBatis(该文通俗易懂,步骤说明详细,是很适合快速上手MyBatis的不二之选)
-
Step4:编写ehcache配置文件
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="D:\Test\ehcache"/> <defaultCache maxElementsInMemory="1000" maxElementsOnDisk="10000000" eternal="false" overflowToDisk="true" timeToIdleSeconds="120" timeToLiveSeconds="120" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"> </defaultCache> </ehcache>
-
Step5:编写logback配置文件
logback.xml:
<?xml version="1.0" encoding="UTF-8"?> <configuration debug="true"> <!-- 指定日志输出的位置 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <!-- 日志输出的格式 --> <!-- 按照顺序分别是: 时间、日志级别、线程名称、打印日志的类、日志主体内容、换行 --> <pattern>[%d{HH:mm:ss.SSS}] [%-5level] [%thread] [%logger] [%msg]%n</pattern> </encoder> </appender> <!-- 设置全局日志级别。日志级别按顺序分别是: DEBUG、INFO、WARN、ERROR --> <!-- 指定任何一个日志级别都只打印当前级别和后面级别的日志。 --> <root level="DEBUG"> <!-- 指定打印日志的appender,这里通过“STDOUT”引用了前面配置的appender --> <appender-ref ref="STDOUT" /> </root> <!-- 根据特殊需求指定局部日志级别 --> <logger name="com.atguigu.crowd.mapper" level="DEBUG"/> </configuration>
-
Step6:编写Java代码
这里可以直接使用前面测试二级缓存的代码,因为他们都是属于二级缓存,所以我们就直接看测试结果吧(●’◡’●)
1)不在中间使用删除方法
2)在中间使用删除方法:
5.3 EHCache配置文件参数详解
参数名 | 作用 |
---|---|
maxElementsInMemory | 设置在内存中缓存的element的最大数目 |
maxElementsOnDisk | 设置在磁盘上缓存的element的最大数目,其中0表示无穷大 |
etenrnal | 设定缓存的elements是否永远不过期。 如果为 true,则缓存的数据始终有效, 如果为false那么还 要根据timeToIdleSeconds、timeToLiveSeconds 判断 |
overflowToDisk | 设定当内存缓存溢出的时候是否将过期的element 缓存到磁盘上 |
timeToIdleSeconds | 当缓存在EhCache中的数据前后两次访问的时间超 过timeToIdleSeconds的属性取值时, 这些数据便 会删除,默认值是0,也就是可闲置时间无穷大 |
timeToLiveSeconds | 缓存element的有效生命期,默认是0.,也就是 element存活时间无穷大 |
diskSpoolBuffer | DiskStore(磁盘缓存)的缓存区大小。默认是 30MB。每个Cache都应该有自己的一个缓冲区 |
diskPersistend | 在VM重启的时候是否启用磁盘保存EhCache中的数 据,默认是false |
diskExpiryThreadIntervalSeconds | 磁盘缓存的清理线程运行间隔,默认是120秒。每 个120s, 相应的线程会进行一次EhCache中数据的 清理工作 |
memoryStoreEvictionPolicy | 当内存缓存达到最大,有新的element加入的时 候, 移除缓存中element的策略。 默认是LRU (最 近最少使用),可选的有LFU (最不常使用)和 FIFO (先进先出) |
备注:颜色加深的是必须参数
推荐阅读:页面置换算法相关概念和计算:含LRU、LFU和FIFO三个算法的详解
参考文章: