简介
什么是缓存
缓存在我们工作生活中经常被提及,比如“怎么清理浏览器的缓存”,“手机内存不够了,如何删除缓存”,“硬盘的缓存是不是越大越好”等等。
其实这些“缓存”可以分为三类:
- 硬件缓存:指的是一块芯片,可以被集成到硬盘或 CPU 上。它用来充当硬盘(CPU)与外界接口(通常是内存)之间的暂存器。利用缓存可以减轻系统的负荷,同时提高数据的传输速率。
- 客户端缓存:某些应用,比如浏览器、微信,为了实现快速响应用户的请求,会把用户之前浏览的东西(文字或图片等)存在本地。在下次访问时,如果本地的缓存里有请求的内容,那么就直接展示出来,不用再次向服务器发送请求,等待服务器响应。
- 服务端缓存:它与客户端缓存目的相同,只不过是站在服务器的角度考虑。如果每次接到客户端请求都要连接一次数据库,当用户请求过多,将会导致负载过大。这时可以把一些经常被请求的数据存放在内存中,当有请求时直接返回,不用连接数据库,这样可以减轻数据库的负担。
关于缓存的定义,总结为一句话就是:缓存是临时存放数据(使用频繁的数据)的地方,介于外部请求和真实数据之间。
为什么要用缓存
- 缓解服务器压力(不用每次去请求资源);
- 提升性能(打开本地资源速度当然比请求回来再打开要快得多);
- 减少带宽消耗;
缓存中的一些术语
命中(HIT):当客户端发起一个请求,如果被请求的资源在缓存中,这个资源就会被使用,我们就叫它缓存命中。
未命中(MISS):当客户端发起一个请求,如果没有在缓存追踪找到,我们称这种 情况为缓存未命中。这时需要查询数据库,并且将查询结果加入缓存中。
存储成本:当未命中时,我们会从数据库中取出数据,然后加入到缓存中。把这个数据放入缓存所需要的时间和空间,就是 存储成本。
失效:当缓存中的数据需要更新时,就意味着当前缓存中的这个数据失效了。缓存中的数据需要同步进行更新操作。还有一种情况就是该缓存过了失效时间。因为缓存会占用内存,缓存量过大会引发别的问题,我们一般都会设置失效时间来让缓存定时过期失效。
失效策略:如果缓存满了,而当前请求又没有命中缓存,那么就会按照某一种策略,把缓存中的某个旧资源剔除,而把新的资源加入缓存。这些决定应该剔除哪个旧资源的策略统称为失效策略(缓存算法)。
常见的一般策略有:
- FIFO(first in first out) :先进先出策略,最先进入缓存的数据在缓存空间不够的情况下(超出最大元素限制)会被优先被清除掉,以腾出新的空间接受新的数据。策略算法主要比较缓存元素的创建时间。在数据实效性要求场景下可选择该类策略,优先保障最新数据可用。
- LFU(less frequently used) :最少使用策略。无论是否过期,根据元素的被使用次数判断,清除使用次数较少的元素释放空间。策略算法主要比较元素的 hitCount(命中次数)。在保证高频数据有效性场景下,可选择这类策略。
- LRU(least recently used) :最近最少使用策略。无论是否过期,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素释放空间。策略算法主要比较元素最近一次被get使用时间。在热点数据场景下较适用,优先保证热点数据的有效性。
除此之外,还有一些简单策略比如:
- 根据过期时间判断,清理过期时间最长的元素;
- 根据过期时间判断,清理最近要过期的元素;
- 随机清理;
- 根据关键字(或元素内容)长短清理等。
Mybatis缓存
Mybatis 缓存属于服务端缓存。
MyBatis 包含一个非常强大的查询缓存特性,它可以非常方便地定制和配置缓存。缓存可以极大的提升查询效率。
MyBatis 系统中默认定义了两级缓存:一级缓存和二级缓存
- 默认情况下,只有一级缓存开启。(SqlSession级别的缓存,也称为本地缓存)
- 二级缓存需要手动开启和配置,它是基于 namespace 级别的缓存,缓存只作用于 cache 标签所在的映射文件中的语句。
一级缓存
一级缓存也叫本地缓存,它仅仅对一个会话中的数据进行缓存,在 Mybatis 中是指 SqlSession 对象开启到关闭的这段时间里称为一个会话。
在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis 提供了一级缓存的方案优化这部分场景,如果是相同的 SQL 语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。具体执行过程如下图所示。
每个 SqlSession 中持有了 Executor,每个 Executor 中有一个 LocalCache。当用户发起查询时,MyBatis 根据当前执行的语句生成 MappedStatement
,在 Local Cache 进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入Local Cache
,最后返回结果给用户。
实例分析
1、在测试项目中加入日志记录,方便查看效果。
2、User 实体类
@Data @AllArgsConstructor @NoArgsConstructor public class User implements Serializable { private int id; private String name; private String password; } 复制代码
3、编写接口
User getUser(@Param("id") int id); 复制代码
4、接口配置文件
<resultMap id="userMap" type="User"> <result property="password" column="pwd" /> </resultMap> <select id="getUser" resultMap="userMap"> select * from mybatis.user where id=#{id} </select> 复制代码
5、mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <!--核心配置文件--> <configuration> <properties resource="db.properties" /> <settings> <setting name="logImpl" value="LOG4J"/> </settings> <typeAliases> <package name="com.msdn.bean"/> </typeAliases> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${driver}"/> <!--jdbc.url=jdbc:mysql://localhost:3306/oto?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC--> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/> </dataSource> </environment> </environments> <mappers> <mapper resource="com/msdn/mapper/UserMapper.xml"/> </mappers> </configuration> 复制代码
6、测试
@Test public void getUser(){ SqlSession sqlSession = MybatisUtil.getSqlSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = userMapper.getUser(1); System.out.println(user); User user2 = userMapper.getUser(1); System.out.println(user); System.out.println(user == user2); sqlSession.close(); } 复制代码
7、结果分析
2020-03-21 20:39:58,472 DEBUG [com.msdn.mapper.UserMapper] - Cache Hit Ratio [com.msdn.mapper.UserMapper]: 0.0 2020-03-21 20:39:58,704 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Preparing: select * from mybatis.user where id=? 2020-03-21 20:39:58,731 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Parameters: 1(Integer) 2020-03-21 20:39:58,747 DEBUG [com.msdn.mapper.UserMapper.getUser] - <== Total: 1 User(id=1, name=hresh, password=123456) 2020-03-21 20:39:58,748 DEBUG [com.msdn.mapper.UserMapper] - Cache Hit Ratio [com.msdn.mapper.UserMapper]: 0.0 User(id=1, name=hresh, password=123456) true 复制代码
通过日志记录可以看出,SQL 语句只执行了一次,第二次获取 User 对象并未查询数据库,最后两个对象比较结果为 true 也说明是同一个对象。
一级缓存失效
一级缓存是默认开启且无法关闭的, 基于 SqlSession 级别。我们说的一级缓存失效,指的是在 SqlSession 对象存活期间,不止一次向数据库发送数据请求。
1、SqlSession 对象不同
@Test public void getUser2(){ SqlSession sqlSession = MybatisUtil.getSqlSession(); SqlSession sqlSession2 = MybatisUtil.getSqlSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = userMapper.getUser(1); System.out.println(user); sqlSession.close(); UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class); User user2 = userMapper2.getUser(1); System.out.println(user); System.out.println(user == user2); sqlSession2.close(); } 复制代码
执行结果为:
2020-03-21 21:45:48,085 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Preparing: select * from mybatis.user where id=? 2020-03-21 21:45:48,118 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Parameters: 1(Integer) 2020-03-21 21:45:48,138 DEBUG [com.msdn.mapper.UserMapper.getUser] - <== Total: 1 User(id=1, name=hresh, password=123456) 2020-03-21 21:45:48,139 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Preparing: select * from mybatis.user where id=? 2020-03-21 21:45:48,140 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Parameters: 1(Integer) 2020-03-21 21:45:48,141 DEBUG [com.msdn.mapper.UserMapper.getUser] - <== Total: 1 User(id=1, name=hresh, password=123456) false 复制代码
从结果中可以看出执行过程中有两条 SQL 语句,可以得出结论 :每个 sqlSession 中的缓存是独立的。
2、SqlSession 对象相同,查询请求不同
@Test public void getUser(){ SqlSession sqlSession = MybatisUtil.getSqlSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = userMapper.getUser(1); System.out.println(user); User user2 = userMapper.getUser(2); System.out.println(user2); System.out.println(user == user2); sqlSession.close(); } 复制代码
执行结果为:
2020-03-21 21:52:34,817 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Preparing: select * from mybatis.user where id=? 2020-03-21 21:52:34,854 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Parameters: 1(Integer) 2020-03-21 21:52:34,871 DEBUG [com.msdn.mapper.UserMapper.getUser] - <== Total: 1 User(id=1, name=hresh, password=123456) 2020-03-21 21:52:34,872 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Preparing: select * from mybatis.user where id=? 2020-03-21 21:52:34,872 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Parameters: 2(Integer) 2020-03-21 21:52:34,874 DEBUG [com.msdn.mapper.UserMapper.getUser] - <== Total: 1 User(id=2, name=hresh2, password=123456) false 复制代码
同样发现有两条 SQL 语句,说明同一 SqlSession 下的缓存中添加新的数据需要请求数据库。
3、SqlSession 相同,两次查询操作之间执行了增删改操作
@Test public void getUser(){ SqlSession sqlSession = MybatisUtil.getSqlSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = userMapper.getUser(1); System.out.println(user); //增删改操作,可能会修改原来的数据,所以必定会刷新缓存 User user1 = new User(4,"acorn22","12344"); userMapper.updateUser(user1); User user2 = userMapper.getUser(1); System.out.println(user2); System.out.println(user == user2); sqlSession.close(); } 复制代码
执行结果:
2020-03-21 21:58:51,685 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Preparing: select * from mybatis.user where id=? 2020-03-21 21:58:51,721 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Parameters: 1(Integer) 2020-03-21 21:58:51,738 DEBUG [com.msdn.mapper.UserMapper.getUser] - <== Total: 1 User(id=1, name=hresh, password=123456) 2020-03-21 21:58:51,740 DEBUG [com.msdn.mapper.UserMapper.updateUser] - ==> Preparing: update mybatis.user set name=? where id=? 2020-03-21 21:58:51,741 DEBUG [com.msdn.mapper.UserMapper.updateUser] - ==> Parameters: acorn22(String), 4(Integer) 2020-03-21 21:58:51,748 DEBUG [com.msdn.mapper.UserMapper.updateUser] - <== Updates: 1 2020-03-21 21:58:51,749 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Preparing: select * from mybatis.user where id=? 2020-03-21 21:58:51,749 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Parameters: 1(Integer) 2020-03-21 21:58:51,751 DEBUG [com.msdn.mapper.UserMapper.getUser] - <== Total: 1 User(id=1, name=hresh, password=123456) false 复制代码
两次查询操作之间执行了修改操作,修改操作后再做任何操作,都会重新请求数据库。说明增删改操作可能会对数据库中的数据产生影响。
4、SqlSession 相同,手动清除一级缓存
@Test public void getUser(){ SqlSession sqlSession = MybatisUtil.getSqlSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = userMapper.getUser(1); System.out.println(user); //手动清理缓存 sqlSession.clearCache(); User user2 = userMapper.getUser(1); System.out.println(user2); System.out.println(user == user2); sqlSession.close(); } 复制代码
执行结果:
2020-03-21 22:03:34,909 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Preparing: select * from mybatis.user where id=? 2020-03-21 22:03:34,947 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Parameters: 1(Integer) 2020-03-21 22:03:34,965 DEBUG [com.msdn.mapper.UserMapper.getUser] - <== Total: 1 User(id=1, name=hresh, password=123456) 2020-03-21 22:03:34,965 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Preparing: select * from mybatis.user where id=? 2020-03-21 22:03:34,966 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Parameters: 1(Integer) 2020-03-21 22:03:34,967 DEBUG [com.msdn.mapper.UserMapper.getUser] - <== Total: 1 User(id=1, name=hresh, password=123456) false 复制代码
可以认为一级缓存是个 map 集合,做了 clear 操作。
二级缓存
在上文中提到的一级缓存中,其最大的共享范围就是一个 SqlSession 内部,如果多个 SqlSession 之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用CachingExecutor 装饰 Executor,进入一级缓存的查询流程前,先在CachingExecutor 进行二级缓存的查询,具体的工作流程如下所示。
二级缓存开启后,同一个 namespace 下的所有操作语句,都影响着同一个 Cache,即二级缓存被多个 SqlSession 共享,是一个全局的变量。
当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。
实例分析
1、在 mybatis-config.xml 中开启全局缓存
<settings> <setting name="cacheEnabled" value="true"/> </settings> 复制代码
2、在对应的 Mapper 配置文件中配置二级缓存
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/> 复制代码
3、测试
首先需要将 JavaBean 类实现序列化接口。
/** * 首先需要开启二级缓存,只在同一个Mapper下有效; * 所有的数据都会先放在一级缓存中; * 只有当会话提交,或关闭时,才会提交到二级缓存中。 * * */ @Test public void getUser2(){ SqlSession sqlSession = MybatisUtil.getSqlSession(); SqlSession sqlSession2 = MybatisUtil.getSqlSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = userMapper.getUser(1); System.out.println(user); sqlSession.close(); UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class); User user2 = userMapper2.getUser(1); System.out.println(user); System.out.println(user == user2); sqlSession2.close(); } 复制代码
执行结果:
2020-03-21 22:22:04,323 DEBUG [com.msdn.mapper.UserMapper] - Cache Hit Ratio [com.msdn.mapper.UserMapper]: 0.0 2020-03-21 22:22:04,668 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Preparing: select * from mybatis.user where id=? 2020-03-21 22:22:04,707 DEBUG [com.msdn.mapper.UserMapper.getUser] - ==> Parameters: 1(Integer) 2020-03-21 22:22:04,729 DEBUG [com.msdn.mapper.UserMapper.getUser] - <== Total: 1 User(id=1, name=hresh, password=123456) 2020-03-21 22:22:04,731 DEBUG [com.msdn.mapper.UserMapper] - Cache Hit Ratio [com.msdn.mapper.UserMapper]: 0.5 User(id=1, name=hresh, password=123456) true 复制代码
结论:
只要开启了二级缓存,在同一个 Mapper 中做的查询,数据都会存放在二级缓存中。数据首先会放在一级缓存中,当 sqlSession 对象提交或关闭后,一级缓存中的数据才会转到二级缓存中。
缓存原理
EhCache
ehcache 是一个用 Java 实现的使用简单,高速,实现线程安全的缓存管理类库,ehcache 提供了用内存,磁盘文件存储,以及分布式存储方式等多种灵活的 cache 管理方案。
使用步骤
1、导入相关依赖
<!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-ehcache --> <dependency> <groupId>org.mybatis.caches</groupId> <artifactId>mybatis-ehcache</artifactId> <version>1.1.0</version> </dependency> 复制代码
2、修改 Mapper 配置文件
<mapper namespace = “org.acme.FooMapper” > <cache type = “org.mybatis.caches.ehcache.EhcacheCache” /> </mapper> 复制代码
3、编写 ehcache.xml 文件,如果在加载时
未找到/ehcache.xml
资源或出现问题,则将使用默认配置。
<?xml version="1.0" encoding="UTF-8"?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd" updateCheck="false"> <!-- diskStore:为缓存路径,ehcache分为内存和磁盘两级,此属性定义磁盘的缓存位置。参数解释如下: user.home – 用户主目录 user.dir – 用户当前工作目录 java.io.tmpdir – 默认临时文件路径 --> <diskStore path="java.io.tmpdir/Tmp_EhCache"/> <!-- defaultCache:默认缓存策略,当ehcache找不到定义的缓存时,则使用这个缓存策略。只能定义一个。 --> <!-- name:缓存名称。 maxElementsInMemory:缓存最大数目 maxElementsOnDisk:硬盘最大缓存个数。 eternal:对象是否永久有效,一但设置了,timeout将不起作用。 overflowToDisk:是否保存到磁盘,当系统当机时 timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。 timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。 diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false. diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。 diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。 memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。 clearOnFlush:内存数量最大时是否清除。 memoryStoreEvictionPolicy:可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。 FIFO,first in first out,这个是大家最熟的,先进先出。 LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。 LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。 --> <defaultCache eternal="false" maxElementsInMemory="10000" overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="1800" timeToLiveSeconds="259200" memoryStoreEvictionPolicy="LRU"/> <cache name="cloud_user" eternal="false" maxElementsInMemory="5000" overflowToDisk="false" diskPersistent="false" timeToIdleSeconds="1800" timeToLiveSeconds="1800" memoryStoreEvictionPolicy="LRU"/> </ehcache>