01月09, 2019

MyBatis手把手跟我做(五) 一级缓存与二级缓存

MyBatis(五) 一级缓存与二级缓存

一.什么是缓存

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

2019-01-08_10-23-51

对于我们之前的JDBC操作,如果需要连续请求id=1的用户数据,那么就需要进行两次的数据库连接,获取数据库中的数据.相同的数据,却需要两次数据库连接,这肯定会造成资源的浪费,相信你肯定不会这么做,只要你稍微有面向对象的知识,你肯定会把第一次获取的数据保存到一个对象中,下一次再直接从对象中获取就行了,也就是下面这个样子 2019-01-08_10-37-18 获取的内容保存在对象中,在一个请求期间,我们直接使用或者传递对象就可以了,JDBC的操作,我们可以自己定义类或者集合来保存数据库中的数据,来避免连续请求数据库的问题.我们用来保存数据的对象或者集合,也能称之为缓存

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

2019-01-08_11-22-55

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

上面这堆接口和类的层次关系是大概是下面这个样子的 2019-01-08_13-36-25

如果觉得太复杂,简单的说,我这里只是想表明一个关键点,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),对于每一次查询,都会尝试根据查询的条件去本地缓存中查找是否在缓存中,如果在缓存中,就直接从缓存中取出,然后返回给用户;否则,从数据库读取数据,将查询结果存入缓存并返回给用户。 2019-01-08_10-37-182 你会发现和我们最开始保存的方式非常类似,只是从一个简单的对象,换成了封装好了的更加复杂的Local Cache对象.

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

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

2019-01-08_14-35-18

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

上面使用的是同一个SqlSession对象,稍微修改一下代码: 2019-01-08_14-48-36 这里通过factory.openSession()等于获取了两个不同的SqlSession对象,注意观察下面的代码: 2019-01-08_14-40-25

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

...
@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);
    //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),因此,必须清空一级缓存

2019-01-08_14-40-25

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

2019-01-08_10-37-11

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

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

1.二级缓存使用场景

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

比如我们要统计排行榜,类似于这种 2019-01-09_10-11-54 像这种排行的查询,可能会涉及到很多张表很多字段的查询统计排序,是非常费时费力的,如果每次都需要去数据库查询显示一次这个排行榜数据,那这个应用基本就没戏了,到查询排行榜这里,必定会卡顿很久,而且这种卡顿是用户不能忍受的,做成一级缓存也是不可行的,每次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: 引用数目,可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的 可用内存资源数目。默认值是 1024readOnly: 只读属性可以被设置为 truefalse。只读的缓存会给所有调用者返回缓 存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存 会返回缓存对象的拷贝(通过序列化) 。这会慢一些,但是安全,因此默认是 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中的全局设置: 2019-01-09_11-08-43

其次,在相关的Mapper 中开启<cache>标签 EmployeeMapper.xml: 2019-01-09_11-11-02

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

2019-01-09_12-41-13

本文链接:http://www.yanhongzhi.com/post/mybatis-cache.html

-- EOF --

Comments