缓存在我们工作生活中经常被提及,比如“怎么清理浏览器的缓存”,“手机内存不够了,如何删除缓存”,“硬盘的缓存是不是越大越好”等等。
其实这些“缓存”可以分为三类:
关于缓存的定义,总结为一句话就是: 缓存是临时存放数据(使用频繁的数据)的地方,介于外部请求和真实数据之间。
命中(HIT):当客户端发起一个请求,如果被请求的资源在缓存中,这个资源就会被使用,我们就叫它缓存命中。
未命中(MISS):当客户端发起一个请求,如果没有在缓存追踪找到,我们称这种 情况为缓存未命中。这时需要查询数据库,并且将查询结果加入缓存中。
存储成本:当未命中时,我们会从数据库中取出数据,然后加入到缓存中。把这个数据放入缓存所需要的时间和空间,就是 存储成本。
失效:当缓存中的数据需要更新时,就意味着当前缓存中的这个数据失效了。缓存中的数据需要同步进行更新操作。还有一种情况就是该缓存过了失效时间。因为缓存会占用内存,缓存量过大会引发别的问题,我们一般都会设置失效时间来让缓存定时过期失效。
失效策略:如果缓存满了,而当前请求又没有命中缓存,那么就会按照某一种策略,把缓存中的某个旧资源剔除,而把新的资源加入缓存。这些决定应该剔除哪个旧资源的策略统称为失效策略(缓存算法)。
常见的一般策略有:
除此之外,还有一些简单策略比如:
Mybatis 缓存属于服务端缓存。
MyBatis 包含一个非常强大的查询缓存特性,它可以非常方便地定制和配置缓存。缓存可以极大的提升查询效率。
MyBatis 系统中默认定义了两级缓存: 一级缓存 和 二级缓存
一级缓存也叫本地缓存,它仅仅对一个会话中的数据进行缓存,在 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 是一个用 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> 复制代码
聊聊MyBatis缓存机制
MyBatis:缓存