第四章、Mybatis的缓存机制

扶醉桌前 提交于 2019-11-29 14:00:51

什么是缓存?

缓存是内存中的一个对象,用于对数据库查询结果的保存,用于减少与数据库的交互次数从而减低数据库的压力,进而提高响应速度。

什么是Mybatis中的缓存?

Mybatis中的缓存就是说Mybatis在执行一次SQL查询或者SQL更新之后,这条SQL语句并不会消失,而是

被Mybatis缓存起来,当再次执行相同SQL语句的时候,就会直接从缓存中进行提取,而不是再次执行SQL命令。

1、缓存机制的简介

(1)MyBatis 包含一个非常强大的查询缓存特性,它可以非常方便地配置和定制。缓存可以极大的提升查询效率

(2)MyBatis系统中默认定义了两级缓存

​ 一级缓存:SqlSession级别的缓存,也称为本地缓存。

​ 二级缓存:基于namespace级别的缓存,也称为表级缓存。全局的作用域

(3)默认情况下,只有一级缓存(SqlSession级别的缓存,也称为本地缓存)开启。

Sqlsession是什么?

​ Sqlsession是SqlsessionFactory会话工厂创建出来的一个会话的对象,这个Sqlsession对象用于执行具体的SQL语句并返回给用户请求的结果。

Sqlsession级别的缓存是什么意思?语句缓存起来,也被称为会话缓存

​ Sqlsession级别的缓存表示的就是每当执行一条SQL语句后,默认就会把该SQL

(4)二级缓存需要手动开启和配置,他是基于namespace级别的缓存。

(5)为了提高扩展性。==MyBatis定义了缓存接口Cache。==我们可以通过实现Cache接口来自定义二级缓存

(6)缓存查找顺序:二级缓存->一级缓存->数据库

2、一级缓存

2.1、一级缓存的使用

(1)一级缓存(local cache), 即本地缓存, 作用域默认为sqlSession。当 Session flush 或 close 后, 该 Session 中的所有 Cache 将被清空。

(2)本地缓存不能被关闭, 但可以调用 clearCache() 来清空本地缓存, 或者改变缓存的作用域.

(3)在mybatis3.1之后, 可以配置本地缓存的作用域. 在 mybatis.xml 中配置

在这里插入图片描述

(4)一级缓存的工作机制(工作原理

​ 同一次会话期间只要查询过的数据都会保存在当前SqlSession的一个Map中

key:

​ hashCode + 查询的SqlId + 编写的sql查询语句 + 参数

(5)一级缓存的好处

在应用运行过程中,有可能在一次数据库会话中,执行多次插叙条件完全相同的SQL

解决方案:

Mybatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。

2.2、一级缓存失效的几种情况

(1)不同的SqlSession对应不同的一级缓存

代码示例:

@Test
	public void testSelectMul() throws IOException {
		SqlSession sqlSession = getSqlSession();//第一个sqlSession
		sqlSession.commit(true);
		
		EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
		Employee employee = employeeMapper.getEmployeeById(3);
		System.out.println(employee);
		
		SqlSession sqlSession2 = getSqlSession();//第二个sqlSession
		sqlSession2.commit(true);
		EmployeeMapper employeeMapper2 = sqlSession2.getMapper(EmployeeMapper.class);
		Employee employee2 = employeeMapper2.getEmployeeById(3);
		System.out.println(employee2);
		
	}
(2)同一个SqlSession但是查询条件不同

代码示例:

@Test
	public void testSelectMul() throws IOException {
		SqlSession sqlSession = getSqlSession();//只有一个Sqlsession
		sqlSession.commit(true);
		
		EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
		Employee employee = employeeMapper.getEmployeeById(3);//查3
		System.out.println(employee);
		
		
		EmployeeMapper employeeMapper2 = sqlSession.getMapper(EmployeeMapper.class);
		Employee employee2 = employeeMapper2.getEmployeeById(4);//查4
		System.out.println(employee2);
		
	}
(3)同一个SqlSession两次查询期间执行了任何一次增删改操作

代码示例:

@Test
public void testSelectMul() throws IOException {
    SqlSession sqlSession = getSqlSession();//只有一个Sqlsession
    sqlSession.commit(true);

    EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
    Employee employee = employeeMapper.getEmployeeById(3);
    System.out.println(employee);
	employeeMapper.updateEmployee(new Employee(null,"wanwu",null,null));//进行了修改操作

    EmployeeMapper employeeMapper2 = sqlSession.getMapper(EmployeeMapper.class);
    Employee employee2 = employeeMapper2.getEmployeeById(3);
    System.out.println(employee2);

}
(4)同一个SqlSession两次查询期间手动清空了缓存

代码示例:

@Test
	public void testSelectMul() throws IOException {
		SqlSession sqlSession = getSqlSession();//只有一个Sqlsession
		sqlSession.commit(true);
		
		EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
		Employee employee = employeeMapper.getEmployeeById(3);
		System.out.println(employee);
		sqlSession.clearCache();//手动清除缓存
		
		EmployeeMapper employeeMapper2 = sqlSession.getMapper(EmployeeMapper.class);
		Employee employee2 = employeeMapper2.getEmployeeById(3);
		System.out.println(employee2);
		
	}

2.3、一级缓存的执行流程

​ 提到一级缓存,那么提到一级缓存就绕不开 SqlSession,所以索性我们就直接从 SqlSession ,看看有没有创建缓存或者与缓存有关的属性或者方法

在这里插入图片描述

调研了一圈,发现上述所有方法中,好像只有 clearCache() 和缓存沾点关系

在这里插入图片描述

那么就直接从这个方法入手吧,分析源码时,我们要看它(此类)是谁,它的父类和子类分别又是谁,对如上关系了解了,你才会对这个类有更深的认识,分析了一圈,

得到如下这个流程图:
在这里插入图片描述

写一段代码:

//接口中的方法
//根据员工id查询单个员工信息
Employee getEmployeeById(Integer id );
<!--SQL映射文件-->

<select id="getEmployeeById" resultType="Employee">
  	select id,last_name,gender,descr from tbl_employee where id=#{id}
</select>
private SqlSession getSqlSession() throws IOException {
    String resource = "mybatis-conf.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession sqlSession = sessionFactory.openSession();
    return sqlSession;
}
	
	
@Test
public void testSelectMul() throws IOException {
    SqlSession sqlSession = getSqlSession();
    sqlSession.commit(true);

    EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
    Employee employee = employeeMapper.getEmployeeById(3);//可以在这个地方设置断点进行debug调试
    System.out.println(employee);

}
	

debug运行之后:

第一步:首先运行到Perpetualcache 中的 clear() 方法,会调用其 cache.clear() 方法

在这里插入图片描述

​ cache 其实就是 private Map<Object, Object> cache = new HashMap<Object, Object>(); 也就是一个Map,所以说 cache.clear() 其实就是 map.clear() ,也就是说,缓存其实就是本地存放的一个 map 对象,每一个SqlSession 都会存放一个 map 对象的引用

第二步:

运行到第一步中clear()方法之后按F6执行下一步就进入到下方的**clearLocalCache()**方法:

在这里插入图片描述

在这里插入图片描述

那么这个 cache 是何时创建的呢?

我觉得是 Executor,为什么这么认为? 因为 Executor 是执行器,用来执行SQL请求,而且清除缓存的方法也在 Executor 中执行,所以很可能缓存的创建也很有可能在 Executor 中,看了一圈发现 Executor 中有一个 createCacheKey 方法,这个方法很像是创建缓存的方法啊,跟进去看看,你发现 createCacheKey 方法是由 BaseExecutor 执行的,代码如下

 @Override
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
//MappedStatement的id
// id 就是Sql语句的所在位置 包名 + 类名 + SQL名称
    cacheKey.update(ms.getId());
// offset 就是 0
    cacheKey.update(rowBounds.getOffset());
// limit 就是 Integer.MAXVALUE
    cacheKey.update(rowBounds.getLimit());
// 具体的SQL语句
    cacheKey.update(boundSql.getSql());
//后面是update了sql中带的参数
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    // mimic DefaultParameterHandler logic
    for (ParameterMapping parameterMapping : parameterMappings) {
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        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 = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        cacheKey.update(value);
      }
    }
    if (configuration.getEnvironment() != null) {
      // issue #176
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

创建缓存key会经过一系列的 update 方法,update 方法由一个 CacheKey 这个对象来执行的,这个 update 方法最终由 updateList 的 list 来把五个值存进去,对照上面的代码和下面的图示,就能理解这五个值都是什么了

在这里插入图片描述

这里需要注意一下最后一个值, configuration.getEnvironment().getId() 这是什么,这其实就是定义在 mybatis-config.xml 中的 标签,见如下。

<!-- 环境配置 -->
	<environments default="xxj">
		<environment id="xxj">
			<transactionManager type="JDBC"/>
			
			<dataSource type="POOLED">
				<property name="driver" value="${jdbc.driver}"/>
				<property name="url" value="${jdbc.url}"/>
				<property name="username" value="${jdbc.user}"/>
				<property name="password" value="${jdbc.password}"/>
			</dataSource>
		
		</environment>
	
	</environments>

那么我们回归正题,那么创建完缓存之后该用在何处呢?总不会凭空创建一个缓存不使用吧?绝对不会的,经过我们对一级缓存的探究之后,我们发现一级缓存更多是用于查询操作,毕竟一级缓存也叫做查询缓存吧,为什么叫查询缓存我们一会儿说。我们先来看一下这个缓存到底用在哪了,我们跟踪到 query 方法如下:

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  BoundSql boundSql = ms.getBoundSql(parameter);
  // 创建缓存
  CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
  return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

@SuppressWarnings("unchecked")
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  ...
  list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
  if (list != null) {
      // 这个主要是处理存储过程用的。
      handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
      list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
  }
  ...
}

// queryFromDatabase 方法
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  List<E> list;
  localCache.putObject(key, EXECUTION_PLACEHOLDER);
  try {
    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
  } finally {
    localCache.removeObject(key);
  }
  localCache.putObject(key, list);
  if (ms.getStatementType() == StatementType.CALLABLE) {
    localOutputParameterCache.putObject(key, parameter);
  }
  return list;
}

如果查不到的话,就从数据库查,在queryFromDatabase中,会对localcache进行写入。localcache 对象的put 方法最终交给 Map 进行存放

private Map<Object, Object> cache = new HashMap<Object, Object>();

@Override
public void putObject(Object key, Object value) {
  cache.put(key, value);
}

2.4、为什么一级缓存叫查询缓存

我们先来看一下 update 更新方法,先来看一下 update 的源码

@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
  ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  clearLocalCache();
  return doUpdate(ms, parameter);
}

由 BaseExecutor 在每次执行 update 方法的时候,都会先 clearLocalCache() ,所以更新方法并不会有缓存,这也就是说为什么一级缓存也叫做查询缓存了,这也就是为什么我们没有探究多次执行更新方法对一级缓存的影响了。

2.5、为什么一级缓存会失效

  1. 探究更新对一级缓存失效的影响: 由上面的分析结论可知,我们每次执行 update 方法时,都会先刷新一级缓存,因为是同一个 SqlSession, 所以是由同一个 Map 进行存储的,所以此时一级缓存会失效

  2. 探究不同的 SqlSession 对一级缓存的影响: 这个也就比较好理解了,因为不同的 SqlSession 会有不同的Map 存储一级缓存,然而 SqlSession 之间也不会共享,所以此时也就不存在相同的一级缓存

  3. 同一个 SqlSession 使用不同的查询操作: 这个论点就需要从缓存的构成角度来讲了,我们通过 cacheKey 可知,一级缓存命中的必要条件是两个 cacheKey 相同,要使得 cacheKey 相同,就需要使 cacheKey 里面的值相同,也就是

在这里插入图片描述在这里插入图片描述

看出差别了吗?第一个SQL 我们查询的是部门编号为1的值,而第二个SQL我们查询的是编号为5的值,两个缓存对象不相同,所以也就不存在缓存。

  1. 手动清理缓存对一级缓存的影响: 由程序员自己去调用clearCache方法,这个方法就是清除缓存的方法,所以也就不存在缓存了。

3、二级缓存

3.1、二级缓存的使用

在这里插入图片描述

(1)二级缓存(second level cache),全局作用域缓存
基于namespace级别的缓存,基于同一个namespace的多个SqlSession对象共享同一个二级缓存
(2)二级缓存默认不开启,需要手动配置
(3)MyBatis提供二级缓存的接口以及实现,缓存实现要求POJO实现Serializable接口
(4)二级缓存使用的步骤:
① 全局配置文件中开启二级缓存
② 需要使用二级缓存的映射文件处使用cache配置缓存
③ 注意:POJO需要实现Serializable接口
(5)二级缓存在 SqlSession 关闭或提交之后才会生效
基于同一个namespace的多个SqlSession对象,第一次查询,先从 缓存中找,如果找不到,则从数据库中查找,将从数据库中查找到的数据存储到当前SqlSession对象的一级缓存中,只有当SqlSession对象进行关闭或者提交操作的时候,才会把一级缓存中的数据存放到二级缓存中

在这里插入图片描述

(6)二级缓存相关的属性
① eviction=“FIFO”:缓存回收策略:
LRU – 最近最少使用的:移除最长时间不被使用的对象。
FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
默认的是 LRU。
② flushInterval:刷新(清空缓存)间隔,单位毫秒
默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新
③ size:引用数目,正整数
代表缓存最多可以存储多少个对象,太大容易导致内存溢出
④ readOnly:只读,true/false
true:只读缓存;会给所有调用者返回缓存对象的相同实例(引用)。因此这些对象不能被修改。这 提供了很重要的性能优势。
false:读写缓存;会返回缓存对象的拷贝(通过序列化)。这会慢一些,但是安全,因此默认是 false。
⑤ type:指定使用的缓存类,一般在自定义缓存或者是整合第三方缓存时要设定

3.2、二级缓存的工作流程

MyBatis 一级缓存最大的共享范围就是一个SqlSession内部,那么如果多个 SqlSession 需要共享缓存,则需要开启二级缓存,开启二级缓存后,会使用 CachingExecutor 装饰 Executor,进入一级缓存的查询流程前,先在CachingExecutor 进行二级缓存的查询,具体的工作流程如下所示

在这里插入图片描述

在这里插入图片描述

当二级缓存开启后,同一个命名空间(namespace) 所有的操作语句,都影响着一个 共同的 cache,也就是二级缓存被多个 SqlSession 共享,是一个全局的变量。当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。

3.3、二级缓存失效的条件

(1)第一次SqlSession 未提交

SqlSession 在未提交的时候,SQL 语句产生的查询结果还没有放入二级缓存中,这个时候 SqlSession2 在查询的时候是感受不到二级缓存的存在的,修改对应的测试类,结果如下:

@Test
public void testSqlSessionUnCommit(){
  //会话过程中第一次发送请求,从数据库中得到结果
  //得到结果之后,mybatis自动将这个查询结果放入到当前用户的一级缓存
  DeptDao dao =  sqlSession.getMapper(DeptDao.class);
  Dept dept = dao.findByDeptNo(1);
  System.out.println("第一次查询得到部门对象 = "+dept);
  //触发MyBatis框架从当前一级缓存中将Dept对象保存到二级缓存

  SqlSession session2 = factory.openSession();
  DeptDao dao2 = session2.getMapper(DeptDao.class);
  Dept dept2 = dao2.findByDeptNo(1);
  System.out.println("第二次查询得到部门对象 = "+dept2);
}

对应的输出结果如下

在这里插入图片描述

(2)更新对二级缓存影响

与一级缓存一样,更新操作很可能对二级缓存造成影响,下面用三个SqlSession来进行模拟,第一个 SqlSession 只是单纯的提交,第二个 SqlSession 用于检验二级缓存所产生的影响,第三个SqlSession 用于执行更新操作,测试如下:

@Test
public void testSqlSessionUpdate(){
  SqlSession sqlSession = factory.openSession();
  SqlSession sqlSession2 = factory.openSession();
  SqlSession sqlSession3 = factory.openSession();

  // 第一个 SqlSession 执行更新操作
  DeptDao deptDao = sqlSession.getMapper(DeptDao.class);
  Dept dept = deptDao.findByDeptNo(1);
  System.out.println("dept = " + dept);
  sqlSession.commit();

  // 判断第二个 SqlSession 是否从缓存中读取
  DeptDao deptDao2 = sqlSession2.getMapper(DeptDao.class);
  Dept dept2 = deptDao2.findByDeptNo(1);
  System.out.println("dept2 = " + dept2);

  // 第三个 SqlSession 执行更新操作
  DeptDao deptDao3 = sqlSession3.getMapper(DeptDao.class);
  deptDao3.updateDept(new Dept(1,"ali","hz"));
  sqlSession3.commit();

  // 判断第二个 SqlSession 是否从缓存中读取
  dept2 = deptDao2.findByDeptNo(1);
  System.out.println("dept2 = " + dept2);
}

对应的输出结果如下

在这里插入图片描述

3.4、多表操作对二级缓存的影响

现有这样一个场景,有两个表,部门表dept(deptNo,dname,loc)和
部门数量表deptNum(id,name,num),其中部门表的名称和部门数量表的名称相同,通过名称能够联查两个表可以知道其坐标(loc)和数量(num),现在我要对部门数量表的
num 进行更新,然后我再次关联dept 和 deptNum 进行查询,你认为这个 SQL 语句能够查询到的 num
的数量是多少?来看一下代码探究一下

public class DeptNum {

    private int id;
    private String name;
    private int num;

    get and set...
}
public class DeptVo {

    private Integer deptNo;
    private String  dname;
    private String  loc;
    private Integer num;

    public DeptVo(Integer deptNo, String dname, String loc, Integer num) {
        this.deptNo = deptNo;
        this.dname = dname;
        this.loc = loc;
        this.num = num;
    }

    public DeptVo(String dname, Integer num) {
        this.dname = dname;
        this.num = num;
    }

    get and set...
    toString()...
}


public interface DeptDao {

     // ...其他方法
    DeptVo selectByDeptVo(String name);

    DeptVo selectByDeptVoName(String name);

    int updateDeptVoNum(DeptVo deptVo);
}
<select id="selectByDeptVo" resultType="com.mybatis.beans.DeptVo">
  select d.deptno,d.dname,d.loc,dn.num from dept d,deptNum dn where dn.name = d.dname
  and d.dname = #{name}
</select>

<select id="selectByDeptVoName" resultType="com.mybatis.beans.DeptVo">
  select * from deptNum where name = #{name}
</select>

<update id="updateDeptVoNum" parameterType="com.mybatis.beans.DeptVo">
  update deptNum set num = #{num} where name = #{dname}
</update>

DeptNum 数据库初始值:

在这里插入图片描述

/**
     * 探究多表操作对二级缓存的影响
     */
@Test
public void testOtherMapper(){

  // 第一个mapper 先执行联查操作
  SqlSession sqlSession = factory.openSession();
  DeptDao deptDao = sqlSession.getMapper(DeptDao.class);
  DeptVo deptVo = deptDao.selectByDeptVo("ali");
  System.out.println("deptVo = " + deptVo);
  // 第二个mapper 执行更新操作 并提交
  SqlSession sqlSession2 = factory.openSession();
  DeptDao deptDao2 = sqlSession2.getMapper(DeptDao.class);
  deptDao2.updateDeptVoNum(new DeptVo("ali",1000));
  sqlSession2.commit();
  sqlSession2.close();
  // 第一个mapper 再次进行查询,观察查询结果
  deptVo = deptDao.selectByDeptVo("ali");
  System.out.println("deptVo = " + deptVo);
}

测试结果如下:

在这里插入图片描述

在对DeptNum 表执行了一次更新后,再次进行联查,发现数据库中查询出的还是
num 为 1050 的值,也就是说,实际上 1050 -> 1000 ,最后一次联查实际上查询的是第一次查询结果的缓存,而不是从数据库中查询得到的值,这样就读到了脏数据。

解决办法

如果是两个mapper命名空间的话,可以使用 来把一个命名空间指向另外一个命名空间,从而消除上述的影响,再次执行,就可以查询到正确的数据

3.5 二级缓存源码解析

3.5.1 二级缓存的创建

(1)二级缓存的创建是使用 Resource 读取 XML 配置文件开始的

InputStream is = Resources.getResourceAsStream("myBatis-config.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
factory = builder.build(is);

(2)读取配置文件后,需要对XML创建 Configuration并初始化

XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());

(3)调用 parser.parse() 解析根目录 /configuration 下面的标签,依次进行解析

public Configuration parse() {
  if (parsed) {
    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
  }
  parsed = true;
  parseConfiguration(parser.evalNode("/configuration"));
  return configuration;
}
private void parseConfiguration(XNode root) {
  try {
    //issue #117 read properties first
    propertiesElement(root.evalNode("properties"));
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    loadCustomVfs(settings);
    typeAliasesElement(root.evalNode("typeAliases"));
    pluginElement(root.evalNode("plugins"));
    objectFactoryElement(root.evalNode("objectFactory"));
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    reflectorFactoryElement(root.evalNode("reflectorFactory"));
    settingsElement(settings);
    // read it after objectFactory and objectWrapperFactory issue #631
    environmentsElement(root.evalNode("environments"));
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    typeHandlerElement(root.evalNode("typeHandlers"));
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}

(4)其中有一个二级缓存的解析就是

mapperElement(root.evalNode("mappers"));

(5)然后进去 mapperElement 方法中

XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();

(6)继续跟 mapperParser.parse() 方法

public void parse() {
  if (!configuration.isResourceLoaded(resource)) {
    configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource);
    bindMapperForNamespace();
  }

  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}

(7)这其中有一个 configurationElement 方法,它是对二级缓存进行创建,如下

private void configurationElement(XNode context) {
  try {
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.equals("")) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
    cacheRefElement(context.evalNode("cache-ref"));
    cacheElement(context.evalNode("cache"));
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    sqlElement(context.evalNodes("/mapper/sql"));
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
  }
}

(8)有两个二级缓存的关键点

cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));

(9)也就是说,mybatis 首先进行解析的是 cache-ref 标签,其次进行解析的是 cache 标签。

根据上面我们的 — 多表操作对二级缓存的影响 一节中提到的解决办法,采用 cache-ref 来进行命名空间的依赖能够避免二级缓存,但是总不能每次写一个 XML 配置都会采用这种方式吧,最有效的方式还是避免多表操作使用二级缓存

然后我们再来看一下cacheElement(context.evalNode(“cache”)) 这个方法

private void cacheElement(XNode context) throws Exception {
  if (context != null) {
    String type = context.getStringAttribute("type", "PERPETUAL");
    Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
    String eviction = context.getStringAttribute("eviction", "LRU");
    Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
    Long flushInterval = context.getLongAttribute("flushInterval");
    Integer size = context.getIntAttribute("size");
    boolean readWrite = !context.getBooleanAttribute("readOnly", false);
    boolean blocking = context.getBooleanAttribute("blocking", false);
    Properties props = context.getChildrenAsProperties();
    builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
  }
}

认真看一下其中的属性的解析,是不是感觉很熟悉?这不就是对 cache 标签属性的解析吗?!!!

上述最后一句代码

builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      boolean blocking,
      Properties props) {
    Cache cache = new CacheBuilder(currentNamespace)
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        .clearInterval(flushInterval)
        .size(size)
        .readWrite(readWrite)
        .blocking(blocking)
        .properties(props)
        .build();
    configuration.addCache(cache);
    currentCache = cache;
    return cache;
  }

这段代码使用了构建器模式,一步一步构建Cache 标签的所有属性,最终把 cache 返回。

3.5.2 二级缓存的使用

在 mybatis 中,使用 Cache 的地方在 CachingExecutor中,来看一下 CachingExecutor 中缓存做了什么工作,我们以查询为例

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
  throws SQLException {
  // 得到缓存
  Cache cache = ms.getCache();
  if (cache != null) {
    // 如果需要的话刷新缓存
    flushCacheIfRequired(ms);
    if (ms.isUseCache() && resultHandler == null) {
      ensureNoOutParams(ms, parameterObject, boundSql);
      @SuppressWarnings("unchecked")
      List<E> list = (List<E>) tcm.getObject(cache, key);
      if (list == null) {
        list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
    }
  }
  // 委托模式,交给SimpleExecutor等实现类去实现方法。
  return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

其中,先从 MapperStatement 取出缓存。只有通过,或@CacheNamespace,@CacheNamespaceRef标记使用缓存的Mapper.xml或Mapper接口(同一个namespace,不能同时使用)才会有二级缓存。

如果缓存不为空,说明是存在缓存。如果cache存在,那么会根据sql配置(,,,的flushCache属性来确定是否清空缓存。

flushCacheIfRequired(ms);

然后根据xml配置的属性useCache来判断是否使用缓存(resultHandler一般使用的默认值,很少会null)。

if (ms.isUseCache() && resultHandler == null)

确保方法没有Out类型的参数,mybatis不支持存储过程的缓存,所以如果是存储过程,这里就会报错。

private void ensureNoOutParams(MappedStatement ms, Object parameter, BoundSql boundSql) {
  if (ms.getStatementType() == StatementType.CALLABLE) {
    for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
      if (parameterMapping.getMode() != ParameterMode.IN) {
        throw new ExecutorException("Caching stored procedures with OUT params is not supported.  Please configure useCache=false in " + ms.getId() + " statement.");
      }
    }
  }
}

然后根据在 TransactionalCacheManager 中根据 key 取出缓存,如果没有缓存,就会执行查询,并且将查询结果放到缓存中并返回取出结果,否则就执行真正的查询方法。

List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
  list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;

3.6 是否应该使用二级缓存?

那么究竟应该不应该使用二级缓存呢?先来看一下二级缓存的注意事项:

  1. 缓存是以namespace为单位的,不同namespace下的操作互不影响。

  2. insert,update,delete操作会清空所在namespace下的全部缓存。

  3. 通常使用MyBatis Generator生成的代码中,都是各个表独立的,每个表都有自己的namespace。

  4. 多表操作一定不要使用二级缓存,因为多表操作进行更新操作,一定会产生脏数据。

    如果你遵守二级缓存的注意事项,那么你就可以使用二级缓存。

    但是,如果不能使用多表操作,二级缓存不就可以用一级缓存来替换掉吗?而且二级缓存是表级缓存,开销大,没有一级缓存直接使用 HashMap 来存储的效率更高,所以==二级缓存并不推荐使用。==

4、缓存的相关属性设置

(1)全局setting的cacheEnable:

​ 配置二级缓存的开关,一级缓存一直是打开的。

(2)select标签的useCache属性:

​ 配置这个select是否使用二级缓存。一级缓存一直是使用的

(3)sql标签的flushCache属性:

​ 增删改默认flushCache=true。sql执行以后,会同时清空一级和二级缓存。

​ 查询默认 flushCache=false。

(4)sqlSession.clearCache():只是用来清除一级缓存。

5、整合第三方缓存

(1)为了提高扩展性。MyBatis定义了缓存接口Cache。我们可以通过实现Cache接口来自定义二级缓存

(2)EhCache 是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认的CacheProvider

(3)整合EhCache缓存的步骤:

​ ① 导入ehcache包,以及整合包,日志包

​ ehcache-core-2.6.8.jar、mybatis-ehcache-1.0.3.jar

​ slf4j-api-1.6.1.jar、slf4j-log4j12-1.6.2.jar

​ ② 编写ehcache.xml配置文件

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
 <!-- 磁盘保存路径 -->
 <diskStore path="D:\atguigu\ehcache" />
 
 <defaultCache 
   maxElementsInMemory="1000" 
   maxElementsOnDisk="10000000"
   eternal="false" 
   overflowToDisk="true" 
   timeToIdleSeconds="120"
   timeToLiveSeconds="120" 
   diskExpiryThreadIntervalSeconds="120"
   memoryStoreEvictionPolicy="LRU">
 </defaultCache>
</ehcache>
 
<!-- 
属性说明:
1、diskStore:指定数据在磁盘中的存储位置。
2、defaultCache:当借助CacheManager.add("demoCache")创建Cache时,EhCache便会采用<defalutCache/>指定的的管理策略
 
以下属性是必须的:
2.1、maxElementsInMemory - 在内存中缓存的element的最大数目 
2.2、maxElementsOnDisk - 在磁盘上缓存的element的最大数目,若是0表示无穷大
2.3、eternal - 设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断
2.4、overflowToDisk - 设定当内存缓存溢出的时候是否将过期的element缓存到磁盘上
 
以下属性是可选的:
2.5、timeToIdleSeconds - 当缓存在EhCache中的数据前后两次访问的时间超过timeToIdleSeconds的属性取值时,这些数据便会删除,默认值是0,也就是可闲置时间无穷大
2.6、timeToLiveSeconds - 缓存element的有效生命期,默认是0.,也就是element存活时间无穷大
 diskSpoolBufferSizeMB 这个参数设置DiskStore(磁盘缓存)的缓存区大小.默认是30MB.每个Cache都应该有自己的一个缓冲区.
2.7、diskPersistent - 在VM重启的时候是否启用磁盘保存EhCache中的数据,默认是false。
l diskExpiryThreadIntervalSeconds - 磁盘缓存的清理线程运行间隔,默认是120秒。每个120s,相应的线程会进行一次EhCache中数据的清理工作
2.8、memoryStoreEvictionPolicy - 当内存缓存达到最大,有新的element加入的时候, 移除缓存中element的策略。默认是LRU(最近最少使用),可选的有LFU(最不常使用)和FIFO(先进先出)
 -->

③ 配置cache标签

org.mybatis.caches.ehcache.EhcacheCache

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!