![e1b71e4302266768e2045374e94fa994.png](https://i-blog.csdnimg.cn/blog_migrate/5dcab7fde927b4eb77b2ebd7b0da3173.jpeg)
一.什么是缓存
要理解MyBatis的一级缓存,至少,你需要先直接什么是缓存的这个概念,其实我们一直都在用
直接来看下面的图:
![4bff690f736f78f31d26042231c31a64.png](https://i-blog.csdnimg.cn/blog_migrate/8e60c6b0b0651b67e3ac53fe7265a2dc.jpeg)
对于我们之前的JDBC操作,如果需要连续请求id=1的用户数据,那么就需要进行两次的数据库连接,获取数据库中的数据.相同的数据,却需要两次数据库连接,这肯定会造成资源的浪费,相信你肯定不会这么做,只要你稍微有面向对象的知识,你肯定会把第一次获取的数据保存到一个对象中,下一次再直接从对象中获取就行了,也就是下面这个样子
![8c47fb72d1a1f90e949088dc646938ba.png](https://i-blog.csdnimg.cn/blog_migrate/a97c0b5a929857496adbfadeb9a8682c.jpeg)
获取的内容保存在对象中,在一个请求期间,我们直接使用或者传递对象就可以了,JDBC的操作,我们可以自己定义类或者集合来保存数据库中的数据,来避免连续请求数据库的问题.我们用来保存数据的对象或者集合,也能称之为缓存
但是我们使用了三层架构之后,就有可能Dao层和Dao层之间互相是不清楚的,如果有一个复杂的业务要在Service层中进行处理,需要分别调用不同Dao层中的数据,那我们这样简单的缓存还是不够看
![bcceab344c3a07809bbc4b007dbb3e74.png](https://i-blog.csdnimg.cn/blog_migrate/3e25c69ec0c996889182c957e24836ce.jpeg)
这种情况,我们要再去处理缓存问题,就会花费我们过多的精力,得不偿失,在这种层面上的缓存处理MyBatis框架已经帮我们做好了,就叫做一级缓存
MyBatis的一级缓存就是基于数据库会话(SqlSession)的
不过个人认为要理解一级缓存,最好先理解一下MyBatis的整体层次架构.如果觉得太复杂,可以直接跳过,这并不影响我们写程序
二. MyBatis的主要层次结构
我们之前使用MyBatis,对数据库操作的代码,能够看见的就是这个SqlSession对象,实际上,这只是MyBatis对外暴露的接口,整个MyBatis核心部件是下面的这么一堆接口和类
- SqlSession —→ MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能
- Executor—→ MyBatis执行器,整个MyBatis调度的核心,负责SQL语句的生成和查询缓存的维护
- StatementHandler —→ 封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合。
- ParameterHandler —→ 负责对用户传递的参数转换成JDBC Statement 所需要的参数,
- ResultSetHandler —→ 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;
- TypeHandler 负责java数据类型和jdbc数据类型之间的映射和转换
- MappedStatement —→ MappedStatement维护了一条
节点的封装, - SqlSource —→ 负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回
- BoundSql —→ 表示动态生成的SQL语句以及相应的参数信息
- Configuration —→ MyBatis所有的配置信息都维持在Configuration对象之中。
上面这堆接口和类的层次关系是大概是下面这个样子的
![905380929f819a6aca8ddac3c904e80a.png](https://i-blog.csdnimg.cn/blog_migrate/b30af9d7936537215438b4122126928f.jpeg)
如果觉得太复杂,简单的说,我这里只是想表明一个关键点,MyBatis对外暴露的接口是SqlSession,而最重要的是Executor接口,只不过这个接口我们平时写代码的时候没有关注而已.Executor的实现类BaseExecutor中拥有一个Cache接口的实现类PerpetualCache,如下:
![13de7a599eb724084a4f87451ce257b3.png](https://i-blog.csdnimg.cn/blog_migrate/db879e040f761efd5ce85a05c9411673.jpeg)
而在PerpetualCache中则有一个HashMap属性:
![0bed7b7c1d05bf444a08a3e8fa0856d9.png](https://i-blog.csdnimg.cn/blog_migrate/ec7d59c2cf9c244693bf78f6fbf11188.jpeg)
当然,这里探讨的是MyBatis源码,我们在这里打住.免得把大家绕晕了
总结就是:
MyBatis封装了JDBC操作,对外给我们暴露了SqlSession接口进行数据库的操作,但是实际MyBatis最核心的接口是Executor,它负责SQL语句的生成和查询缓存的维护,如果没有缓存就查数据库,有缓存就使用的是PerpetualCache中的HashMap保存的数据缓存.最终的最终, MyBatis的一级缓存其实就保存在一个HashMap中
那么HashMap中又是怎么判断查询方法是否相同了呢?其实主要是通过HashMap的key值
BaseExecutor.java:
...public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) { if (this.closed) { throw new ExecutorException("Executor was closed.");
} else {
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
Iterator var8 = parameterMappings.iterator(); while(var8.hasNext()) {
ParameterMapping parameterMapping = (ParameterMapping)var8.next(); if (parameterMapping.getMode() != ParameterMode.OUT) {
String propertyName = parameterMapping.getProperty();
Object value; if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = this.configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
} if (this.configuration.getEnvironment() != null) {
cacheKey.update(this.configuration.getEnvironment().getId());
} return cacheKey;
}
}
...
从上面代码中可以看出,如果下面条件一样,就可以判断为两个相同的查询:
- statementId
- RowBounds的offset、limit的结果集分页属性;
- SQL语句;
- 传给JDBC的参数值
看不懂没关系…如果以后面试题遇到了说说就可以了
三.MyBatis的一级缓存
1. 一级缓存最简单的组织形式
下面展示的是一级缓存存储的基本形式: MyBatis会在一次会话的表示——一个SqlSession对象中创建一个本地缓存(local cache),对于每一次查询,都会尝试根据查询的条件去本地缓存中查找是否在缓存中,如果在缓存中,就直接从缓存中取出,然后返回给用户;否则,从数据库读取数据,将查询结果存入缓存并返回给用户。
![fd4ce60c3349f7b869aa62d374e3a061.png](https://i-blog.csdnimg.cn/blog_migrate/100cca9c0a721dd176e7dc08f26d47b3.jpeg)
你会发现和我们最开始保存的方式非常类似,只是从一个简单的对象,换成了封装好了的更加复杂的Local Cache对象.
实际上, SqlSession只是一个MyBatis对外的接口,SqlSession将它的工作交给了Executor执行器这个角色来完成,负责完成对数据库的各种操作。当创建了一个SqlSession对象时,MyBatis会为这个SqlSession对象创建一个新的Executor执行器,而缓存信息就被维护在这个Executor执行器中,MyBatis将缓存和对缓存相关的操作封装在Cache接口中。他们之间的组织关系,大概如下图:
![33d469ed752a636b447149ffd155bbb9.png](https://i-blog.csdnimg.cn/blog_migrate/7d92a45ce9ec3045b188555764837f9e.jpeg)
2.一级缓存的生命周期
上面费了那么多话…其实主要原因在于…MyBatis已经默认帮我们打开了一级缓存,不需要我们做任何设置,直接就可以用,所以多花心思介绍了一下一级缓存的原理.我们在之前的工程中新建一个测试类,来看一下一级缓存的效果:
public class Test2 { private static Logger log = Logger.getLogger(Test.class); //测试一级缓存1
@org.junit.Test public void testLocalCache1(){
String resource = "mybatis-configuration.xml";
InputStream is = this.getClass().getClassLoader().getResourceAsStream(resource);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = factory.openSession();
System.out.println("--------------------------------------根据主键id=1查询用户信息 开始--------------------------------------");
String stmt = "com.yingside.mapper.UserMapper.getUser";
User user1 = sqlSession.selectOne(stmt,1);
log.info(user1);
System.out.println("--------------------------------------根据主键id=1查询用户信息 结束--------------------------------------"); //相同的id再次查询
System.out.println("--------------------------------------根据主键id=1查询用户信息 开始--------------------------------------");
User user2 = sqlSession.selectOne(stmt,1);
log.info(user2);
System.out.println("--------------------------------------根据主键id=1查询用户信息 结束--------------------------------------");
}
sqlSession.close();
}
![f69daa1036b4f540b5e21a873fe1f6ae.png](https://i-blog.csdnimg.cn/blog_migrate/5f754854b9d6bc3e50a989954a43924e.jpeg)
通过上面的图很容易看出,第二次我们查询的时候明显没有再去执行数据库的操作,只是从一级缓存中读取了User对象的信息
上面使用的是同一个SqlSession对象,稍微修改一下代码:
![5afca3b8c212ef3982d014ee73d75840.png](https://i-blog.csdnimg.cn/blog_migrate/0a01610ef93186f52945d31f965a5534.jpeg)
这里通过factory.openSession()
等于获取了两个不同的SqlSession对象,注意观察下面的代码:
![1c315b54983bedc8c08937e279c1bfbc.png](https://i-blog.csdnimg.cn/blog_migrate/fa5473e803ef0d7df7fc09672767dddd.jpeg)
或者,我们再修改一下源代码,第一次查询完成之后,执行sqlSession.clearCache();
或者sqlSession.commit();
...@org.junit.Testpublic void testLocalCache1(){
String resource = "mybatis-configuration.xml";
InputStream is = this.getClass().getClassLoader().getResourceAsStream(resource);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = factory.openSession();
System.out.println("--------------------------------------根据主键id=1查询用户信息 开始--------------------------------------");
String stmt = "com.yingside.mapper.UserMapper.getUser";
User user1 = sqlSession.selectOne(stmt,1);
log.info(user1); //sqlSession.close();
System.out.println("--------------------------------------根据主键id=1查询用户信息 结束--------------------------------------"); //手动清空一级缓存
//sqlSession.clearCache();
//提交事务,实际也会清空一级缓存
sqlSession.commit(); //相同的id再次查询
System.out.println("--------------------------------------根据主键id=1查询用户信息 开始--------------------------------------"); //打开一个新的sqlSession,也就会有一个新的一级缓存了,因为一级缓存是依附于SqlSession对象的
//sqlSession = factory.openSession();
User user2 = sqlSession.selectOne(stmt,1);
log.info(user2);
System.out.println("--------------------------------------根据主键id=1查询用户信息 结束--------------------------------------");
sqlSession.close();
}
...
sqlSession.clearCache(): 手动清空一级缓存sqlSession.commit(): 实际是执行增删改操作时候的事务提交,但是在事务提交的同时,清空一级缓存,因为执行增删改操作之后,很可能就会存在脏数据(Dirty Read),因此,必须清空一级缓存
![1c315b54983bedc8c08937e279c1bfbc.png](https://i-blog.csdnimg.cn/blog_migrate/fa5473e803ef0d7df7fc09672767dddd.jpeg)
这里就很明显,这两种修改都执行了两次查询,这就是我们一级缓存要注意的第一个点,一级缓存的生命周期
![3a7bcfe52e08ee5d76979c9ce9eeb16f.png](https://i-blog.csdnimg.cn/blog_migrate/48d6038f4e47d2c999f2e813c13439c9.jpeg)
- MyBatis在开启一个数据库会话时,会 创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象(Cache接口的实现类);当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
- 如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用;
- 如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是SqlSession对象仍可使用;
- SqlSession中执行了任何一个增删改操作(update()、delete()、insert())之后执行事务提交commit() ,都会清空PerpetualCache对象的数据,但是SqlSession对象可以继续使用;
MyBatis的二级缓存
首先来说,一级缓存是基于SqlSession对象的,也就是一次数据库会话期间,而二级缓存是则是基于全局的
![1611fd169d539a18102897660443d7c8.png](https://i-blog.csdnimg.cn/blog_migrate/99c650bc974774d5fee7c92692251d8d.jpeg)
上面的图说明的二级缓存的存在形式
1.二级缓存使用场景
可能大家自己在做测试或者说学习的时候,基本接触不到二级缓存,首先大家看看这种查询场景
比如我们要统计排行榜,类似于这种
![cf273c1029833469ba9a0b33c29e5771.png](https://i-blog.csdnimg.cn/blog_migrate/6cba8e83aa35f2706791d8944cbe2450.jpeg)
像这种排行的查询,可能会涉及到很多张表很多字段的查询统计排序,是非常费时费力的,如果每次都需要去数据库查询显示一次这个排行榜数据,那这个应用基本就没戏了,到查询排行榜这里,必定会卡顿很久,而且这种卡顿是用户不能忍受的,做成一级缓存也是不可行的,每次SqlSession请求,每个客户上来难道都要卡顿一次吗?所以,这种查询肯定要做成全局的缓存,当应用启动的时候就缓存这种查询数据,然后每一周刷新一次这种数据就可以了
所以,根据上面场景的分析,我们可以简单的用一句话总结二级缓存的特点和使用场景:
二级缓存作用于全局,对于一些相当消耗性能的,并且对于时效性不明感的查询我们可以使用二级缓存
而且注意,如果开启了二级缓存,我们查询的属性是下面这样
二级缓存 —→ 一级缓存 —→ 数据库
2.MyBatis二级缓存的配置
在MyBatis中使用二级缓存就必须要进行配置了,必须要有下面的步骤才能正常使用二级缓存
(1). 在全局设置中开启二级缓存
<settings>...<!-- 开启二级缓存 --><setting name="cacheEnabled" value="true"/>...</settings>
(2). 在XXXMapper.xml中开启<cache>
标签
<cache eviction="FIFO" flushInterval="60000" readOnly="false" size="1024"></cache>
上面这句话,其实可以简单写为:
<cache />
这样就表示在Mapper.xml中开启二级缓存了,因为<cache>
标签的每个属性都有默认值
cache标签属性:
eviction: 缓存回收策略,这个属性又有下面几个值
LRU – 最近最少使用的:移除最长时间不被使用的对象。
FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
默认是LRU
flushInterval: 刷新间隔,可以被设置为任意的正整数,而且它们代表一个合理的毫秒 形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。
size: 引用数目,可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的 可用内存资源数目。默认值是 1024。
readOnly: 只读属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓 存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存 会返回缓存对象的拷贝(通过序列化) 。这会慢一些,但是安全,因此默认是 false。
(3). 相关实体类需要序列化
也就是需要放入二级缓存中保存的JavaBean需要实现Serializable
接口
(4). useCache和flushCache
这一步不是必须的.这两个都是属于查询标签<select>
的属性
userCache是用来设置是否禁用二级缓存的,在statement中设置useCache=false可以禁用当前select语句的二级缓存,即每次查询都会发出sql去查询,默认情况是true,即该sql使用二级缓存。
flushCache属性,默认情况下为true,即刷新缓存,如果改成false则不会刷新。使用缓存时如果手动修改数据库表中的查询数据会出现脏读。
说了这么多,我们就来看一看使用二级缓存的例子,以及使用二级缓存之后会造成的问题
3. 使用二级缓存示例
首先清楚我们要干什么?
我这里要执行这几个步骤来主要是来观察二级缓存所造成的问题
- 根据员工的主键id,级联查询员工和部门信息
- 修改与员工相关的部门名称
- 再次级联查询这名员工与部门信息
先根据我们之前的步骤开启二级缓存
首先是在mybatis-configuration.xml
中的全局设置:
![9757829055bb684805c6f3e2523dcc24.png](https://i-blog.csdnimg.cn/blog_migrate/3a5a65cc832bccba63c4b1c8f7deade0.jpeg)
其次,在相关的Mapper 中开启<cache>
标签EmployeeMapper.xml:
![01c16820da00bbb245713b188bfe8a8a.png](https://i-blog.csdnimg.cn/blog_migrate/05dca4cdb15e479a83f10f0a37863a13.jpeg)
然后,相关的javabean实现Serializable
接口Employee.java:
public class Employee implements Serializable { private int empId; private String empName; private String empTel; private String empEducation; private Date empBirthday; private Dept dept;
......
Dept.java:
public class Dept implements Serializable { private int deptId; private String deptName; private String deptInfo; private Date deptCreateDate; private List<Employee> employeeList;
......
为了实现部门信息的更改,在DeptMapper.xml加入修改的代码:
DeptMapper.xml:
......<!-- 根据 id 更新 t_dept 表的数据 --><update id="updateDeptByIdSelective" parameterType="dept">
update t_dept <trim prefix="set" suffixOverrides=",">
<if test="deptName != null">
dept_name=#{deptName}, </if>
<if test="deptInfo != null">
dept_info=#{deptInfo}, </if>
<if test="deptCreateDate != null">
dept_createDate=#{deptCreateDate} </if>
</trim>
where dept_id=#{deptId}</update>......
最后,在测试类中加入测试函数Test2.java:
...... //测试二级缓存@org.junit.Testpublic void testLocalCache2(){
String resource = "mybatis-configuration.xml";
InputStream is = this.getClass().getClassLoader().getResourceAsStream(resource);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = factory.openSession(); //根据员工主键id查询
System.out.println("--------------------------------------根据员工主键id=1查询员工信息并级联部门信息 开始--------------------------------------");
String stmt = "com.yingside.mapper.EmployeeMapper.getEmployeeById";
Employee employee1 = sqlSession.selectOne(stmt,1);
log.info(employee1);
System.out.println("--------------------------------------根据员工主键id=1查询员工信息并级联部门信息 结束--------------------------------------"); //部门修改
System.out.println("--------------------------------------根据部门id=2更新部门信息 开始--------------------------------------");
String stmt2 = "com.yingside.mapper.DeptMapper.updateDeptByIdSelective";
Dept dept = new Dept();
dept.setDeptId(2);
dept.setDeptName("人事部");
dept.setDeptCreateDate(new Date());
dept.setDeptInfo("员工薪资,员工激励,员工招聘,团队建设"); int n = sqlSession.update(stmt2,dept);
sqlSession.commit();
log.info(n);
System.out.println("--------------------------------------根据部门id=2更新部门信息 结束--------------------------------------"); //相同的id再次查询
System.out.println("--------------------------------------根据员工主键id=1查询员工信息并级联部门信息 开始--------------------------------------");
Employee employee2 = sqlSession.selectOne(stmt,1);
log.info(employee2);
System.out.println("--------------------------------------根据员工主键id=1查询员工信息并级联部门信息 结束--------------------------------------");
}
......
![9035b7a4e8dfcfd5ff912b5c98987989.png](https://i-blog.csdnimg.cn/blog_migrate/a1470a334a5b69b7d85e85c2ad6e64f6.jpeg)
原文转自朗沃易课堂,更多详情可以搜索“朗沃”关注更多!