700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > mybatis 三级缓存查询循序_MyBatis手把手跟我做系列(五) --- 一级缓存与二级缓存

mybatis 三级缓存查询循序_MyBatis手把手跟我做系列(五) --- 一级缓存与二级缓存

时间:2022-05-06 00:30:23

相关推荐

mybatis 三级缓存查询循序_MyBatis手把手跟我做系列(五) --- 一级缓存与二级缓存

一.什么是缓存

要理解MyBatis的一级缓存,至少,你需要先直接什么是缓存的这个概念,其实我们一直都在用

直接来看下面的图:

对于我们之前的JDBC操作,如果需要连续请求id=1的用户数据,那么就需要进行两次的数据库连接,获取数据库中的数据.相同的数据,却需要两次数据库连接,这肯定会造成资源的浪费,相信你肯定不会这么做,只要你稍微有面向对象的知识,你肯定会把第一次获取的数据保存到一个对象中,下一次再直接从对象中获取就行了,也就是下面这个样子

获取的内容保存在对象中,在一个请求期间,我们直接使用或者传递对象就可以了,JDBC的操作,我们可以自己定义类或者集合来保存数据库中的数据,来避免连续请求数据库的问题.我们用来保存数据的对象或者集合,也能称之为缓存

但是我们使用了三层架构之后,就有可能Dao层和Dao层之间互相是不清楚的,如果有一个复杂的业务要在Service层中进行处理,需要分别调用不同Dao层中的数据,那我们这样简单的缓存还是不够看

这种情况,我们要再去处理缓存问题,就会花费我们过多的精力,得不偿失,在这种层面上的缓存处理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对象之中。

上面这堆接口和类的层次关系是大概是下面这个样子的

如果觉得太复杂,简单的说,我这里只是想表明一个关键点,MyBatis对外暴露的接口是SqlSession,而最重要的是Executor接口,只不过这个接口我们平时写代码的时候没有关注而已.Executor的实现类BaseExecutor中拥有一个Cache接口的实现类PerpetualCache,如下:

而在PerpetualCache中则有一个HashMap属性:

当然,这里探讨的是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;}}...

从上面代码中可以看出,如果下面条件一样,就可以判断为两个相同的查询:

statementIdRowBounds的offset、limit的结果集分页属性;SQL语句;传给JDBC的参数值

看不懂没关系…如果以后面试题遇到了说说就可以了

三.MyBatis的一级缓存

1. 一级缓存最简单的组织形式

下面展示的是一级缓存存储的基本形式:MyBatis会在一次会话的表示——一个SqlSession对象中创建一个本地缓存(local cache),对于每一次查询,都会尝试根据查询的条件去本地缓存中查找是否在缓存中,如果在缓存中,就直接从缓存中取出,然后返回给用户;否则,从数据库读取数据,将查询结果存入缓存并返回给用户。

你会发现和我们最开始保存的方式非常类似,只是从一个简单的对象,换成了封装好了的更加复杂的Local Cache对象.

实际上, SqlSession只是一个MyBatis对外的接口,SqlSession将它的工作交给了Executor执行器这个角色来完成,负责完成对数据库的各种操作。当创建了一个SqlSession对象时,MyBatis会为这个SqlSession对象创建一个新的Executor执行器,而缓存信息就被维护在这个Executor执行器中,MyBatis将缓存和对缓存相关的操作封装在Cache接口中。他们之间的组织关系,大概如下图:

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();}

通过上面的图很容易看出,第二次我们查询的时候明显没有再去执行数据库的操作,只是从一级缓存中读取了User对象的信息

上面使用的是同一个SqlSession对象,稍微修改一下代码:

这里通过factory.openSession()等于获取了两个不同的SqlSession对象,注意观察下面的代码:

或者,我们再修改一下源代码,第一次查询完成之后,执行sqlSession.clearCache();或者mit();

...@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();//提交事务,实际也会清空一级缓存mit(); //相同的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():手动清空一级缓存mit():实际是执行增删改操作时候的事务提交,但是在事务提交的同时,清空一级缓存,因为执行增删改操作之后,很可能就会存在脏数据(Dirty Read),因此,必须清空一级缓存

这里就很明显,这两种修改都执行了两次查询,这就是我们一级缓存要注意的第一个点,一级缓存的生命周期

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对象的,也就是一次数据库会话期间,而二级缓存是则是基于全局的

上面的图说明的二级缓存的存在形式

1.二级缓存使用场景

可能大家自己在做测试或者说学习的时候,基本接触不到二级缓存,首先大家看看这种查询场景

比如我们要统计排行榜,类似于这种

像这种排行的查询,可能会涉及到很多张表很多字段的查询统计排序,是非常费时费力的,如果每次都需要去数据库查询显示一次这个排行榜数据,那这个应用基本就没戏了,到查询排行榜这里,必定会卡顿很久,而且这种卡顿是用户不能忍受的,做成一级缓存也是不可行的,每次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 – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。默认是LRUflushInterval: 刷新间隔,可以被设置为任意的正整数,而且它们代表一个合理的毫秒 形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。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中的全局设置:

其次,在相关的Mapper 中开启<cache>标签EmployeeMapper.xml:

然后,相关的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);mit();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查询员工信息并级联部门信息 结束--------------------------------------");}......

原文转自朗沃易课堂,更多详情可以搜索“朗沃”关注更多!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。