一.什么是缓存

要理解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;}}
...

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

  1. statementId
  2. RowBounds的offset、limit的结果集分页属性;
  3. SQL语句;
  4. 传给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();或者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),因此,必须清空一级缓存

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

  1. MyBatis在开启一个数据库会话时,会 创建一个新的SqlSession对象,SqlSession对象中会有一个新的Executor对象,Executor对象中持有一个新的PerpetualCache对象(Cache接口的实现类);当会话结束时,SqlSession对象及其内部的Executor对象还有PerpetualCache对象也一并释放掉。
  2. 如果SqlSession调用了close()方法,会释放掉一级缓存PerpetualCache对象,一级缓存将不可用;
  3. 如果SqlSession调用了clearCache(),会清空PerpetualCache对象中的数据,但是SqlSession对象仍可使用;
  4. 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. 使用二级缓存示例

首先清楚我们要干什么?
我这里要执行这几个步骤来主要是来观察二级缓存所造成的问题

  1. 根据员工的主键id,级联查询员工和部门信息
  2. 修改与员工相关的部门名称
  3. 再次级联查询这名员工与部门信息

先根据我们之前的步骤开启二级缓存
首先是在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);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查询员工信息并级联部门信息 结束--------------------------------------");
}
......

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

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

  1. mybatis级联查询list_MyBatis手把手跟我做系列(四) ---级联查询与懒加载

    涉及到数据库的级联查询,那肯定就要提到一对一,一对多,多对多这样的表关系,以及java程序与之对应的类和类之间的表现形式,我这里主要通过一对多的表关系给大家介绍以下MyBatis里面对于级联关系的处理 ...

  2. mybatis 使用in 查询时报错_MyBatis(四):mybatis中使用in查询时的注意事项

    packagecom.boco.jobmonitor.model;importjava.util.Date;importjavax.persistence.GeneratedValue;importj ...

  3. 关于IAP支付,谷歌和苹果订阅商品——最白话,手把手教你做系列。

    简述:最近项目要接入订阅商品,这里总结一下公司大佬们的经验和我整理后脚本. 一.关于订阅 1,跟消耗性和非消耗性的购买类似,开发者账后后台建订阅型商品. 订阅型商品分两种,自动续订和非自动续订的,一般 ...

  4. Unity内IAP支付二次验证/服务器验证————最白话,手把手教你做系列。

    之前的一篇写了Unity支付的IAP支付接入. 后来就出现了一些问题,数据统计的时候出现大量购买订单.但是实际上账户的钱却没有增加.@¥--&¥--*@¥&@初步判定可能存在部分用户通 ...

  5. C#获取目录下所有文件的列表——最白话,手把手教你做系列。

    最近,在试着做一个Unity自动生成Ui的功能. 理论比较简单,通过拼接字符串生成脚本,都是Ok的,但为了更方便,就想顺便把文件路径也自动生成一下.即获取某个目录下所有预制体的文件目录. 于是就需要一 ...

  6. Vungle激励广告接入——最白话,手把手教你做系列。

    首先Vungle是个广告插件.好像暂时只支持视频激励广告. 然后,申请ID不在赘述,要注意的是Vungle需要申请两个ID.对应环境初始化的ID和"VunglePlacementID&quo ...

  7. 在正方体的某一个面上随机一个点——最白话,手把手教你做系列。

    问题需求 其实就是某个物体在随机运动,如果运动到正方形的某个面的上方的话,就在这个面上随机出来一个点降落下去. 解决方案 步骤1:物体向六个方向发射射线检测正方体位置. 射线检测,很简单,一句代码就不 ...

  8. Mobvista广告接入——最白话,手把手教你做系列。

    Mobvista广告接入Unity.主要讲插屏,视频广告以及Native的使用. 首先Mobvista广告得SDK目前应该只能通过由服务商提供,必须有Mobvista平台的账号. 下面开始接Mobvi ...

  9. mybatis教程--查询缓存(一级缓存二级缓存和整合ehcache)

    查询缓存 1 缓存的意义 将用户经常查询的数据放在缓存(内存)中,用户去查询数据就不用从磁盘上(关系型数据库数据文件)查询,从缓存中查询,从而提高查询效率,解决了高并发系统的性能问题. 2 mybat ...

最新文章

  1. python语言中如何使用注释
  2. SVD分解算法及其应用
  3. Nacos在双击startup.cmd启动时提示:Unable to start embedded Tomcat
  4. Druid 配置_StatFilter
  5. Postman里如何把某个HTTP的请求和响应作为example保存
  6. 怎样使用orapwd新建口令文件
  7. ios跨线程通知_一种基于Metal、Vulkan多线程渲染能力的渲染架构
  8. python输入三个整数、输出最大的数_题目:使用Python编程,输入三个整数x,y,z,请把这三个数由小到大输出...
  9. everything搭配什么软件_带你飞起来的好工具Everything,极速找到你所要的文件
  10. 数据挖掘:模型选择——K-means
  11. 通过FD耗尽实验谈谈使用HttpClient的正确姿势 1
  12. 注册表知识and技巧大全
  13. 张小龙的30条产品法则
  14. win10开机启动慢如何解决?三种方法帮你解决!
  15. 小米路由器4刷padavan固件
  16. 酒店管理系统的设计与实现/酒店客房管理系统/酒店预定系统
  17. 9月14日更新的sublime激活码
  18. 阿里云OCR图片识别
  19. 【机械常识】螺栓、螺钉、螺柱、螺杆、螺母的区别和归类
  20. 计算机中的物理知识点总结,有关初中物理电磁波章节知识点总结

热门文章

  1. 低级问题---.net franmework安装
  2. 【01】blockqote美化
  3. PL/SQL Developer-官网下载地址
  4. 【转】wpa_supplicant与wpa_cli之间通信过程
  5. Windows Workflow Foundation实验01——Windows Workflow Foundation快速入门(练习四)
  6. 如何解决markdown中图片上传的问题
  7. 解决Flash挡住层用z-index无效的问题
  8. directx11编程中遇到的错误及解决方法
  9. 解决VS+QT无法生成moc文件的问题
  10. 当自己颓废的时候怎么激励自己?