MyBatis 一级缓存和二级缓存存在的问题和原理源码介绍

版本:mybatis-3.5.4

mybatis的单元测试采用HSQLDB

HSQLDB官方文档:http://hsqldb.org/doc/2.0/guide/index.html。

 

缓存是MyBatis中非常重要的特性。在应用程序和数据库都是单节点的情况下,合理使用缓存能够减少数据库IO,显著提升系统性能。但是在分布式环境下,如果使用不当,则可能会带来数据一致性问题。MyBatis提供了一级缓存和二级缓存,其中一级缓存基于SqlSession实现,而二级缓存基于Mapper实现。

MyBatis 中两级缓存都是依赖于基础支持层中的缓存模块实现的。MyBatis 中自带的这两级缓存与 MyBatis 以及整个应用是运行在同一个 JVM 中的,共享同一块堆内存。如果这两级缓存中的数据量较大, 则可能影响系统中其他功能的运行,所以当需要缓存大量数据时,优先考虑使用 Redis、Memcache 等缓存产品。

看完文章,你将了解以下问题:

1、mybatis一级缓存为什么不能关闭?对数据一致性要求比较高的场景如果做?

2、为什么dao层只有接口没有实现类?

 

讲缓存之前,先简单讲一下mybatis组件构成:

mybatis组件介绍

 

  • Configuration:用于描述MyBatis的主配置信息,其他组件需要获取配置信息时,直接通过Configuration对象获取。除此之外,MyBatis在应用启动时,将Mapper配置信息、类型别名、TypeHandler等注册到Configuration组件中,其他组件需要这些信息时,也可以从Configuration对象中获取。
  • MappedStatement:MappedStatement用于描述Mapper中的SQL配置信息,是对Mapper XML配置文件中<select|update|delete|insert>等标签或者@Select/@Update等注解配置信息的封装。
  • SqlSession:SqlSession是MyBatis提供的面向用户的API,表示和数据库交互时的会话对象,用于完成数据库的增删改查功能。SqlSession是Executor组件的外观,目的是对外提供易于理解和使用的数据库操作接口。Spring事物和SqlSession绑定的,每一个事物都是单独的SqlSession。
  • Executor:Executor是MyBatis的SQL执行器,MyBatis中对数据库所有的增删改查操作都是由Executor组件完成的。
  • StatementHandler:StatementHandler封装了对JDBCStatement对象的操作,比如为Statement对象设置参数,调用Statement接口提供的方法与数据库交互,等等。
  • ParameterHandler:当MyBatis框架使用的Statement类型为CallableStatement和PreparedStatement时,ParameterHandler用于为Statement对象参数占位符设置值。
  • ResultSetHandler:ResultSetHandler封装了对JDBC中的ResultSet对象操作,当执行SQL类型为SELECT语句时,ResultSetHandler用于将查询结果转换成Java对象。
  • TypeHandler:TypeHandler是MyBatis中的类型处理器,用于处理Java类型与JDBC类型之间的映射。它的作用主要体现在能够根据Java类型调用PreparedStatement或CallableStatement对象对应的setXXX()方法为Statement对象设置值,而且能够根据Java类型调用ResultSet对象对应的getXXX()获取SQL执行结果。

 

问:mybatis dao层只有接口,实现类怎么来?

**Mapper是一个接口,我们调用SqlSession对象getMapper()返回的到底是什么呢?

SqlSession执行Mappper过程

1、Mapper接口的注册:解析XML或者注解

接口方法名一般和MapperXML配置文件中<select|update|delete|insert>标签的id属性相同

final AutoConstructorMapper mapper = sqlSession.getMapper(AutoConstructorMapper.class);

 

实现过程:MyBatis中通过MapperProxy类实现动态代理,invoke()方法中为通用的拦截逻辑。使用MapperProxyFactory创建Mapper动态代理对象。MapperProxyFactory类的工厂方法newInstance()是非静态的。也就是说,使用MapperProxyFactory创建Mapper动态代理对象首先需要创建MapperProxyFactory实例,而MapperProxyFactory在启动时会创建加载到Configuration对象。MyBatis通过mapperRegistry属性注册Mapper接口与MapperProxyFactory对象之间的对应关系。

整体流程:

MyBatis中Mapper的配置分为两部分,分别为Mapper接口和MapperSQL配置。MyBatis通过动态代理的方式创建Mapper接口的代理对象,MapperProxy类中定义了Mapper方法执行时的拦截逻辑,通过MapperProxyFactory创建代理实例,MyBatis启动时,会将MapperProxyFactory注册到Configuration对象中。另外,MyBatis通过MappedStatement类描述Mapper SQL配置信息,框架启动时,会解析Mapper SQL配置,将所有的MappedStatement对象注册到Configuration对象中。通过Mapper代理对象调用Mapper接口中定义的方法时,会执行MapperProxy类中的拦截逻辑,将Mapper方法的调用转换为调用SqlSession提供的API方法。在SqlSession的API方法中通过Mapper的Id找到对应的MappedStatement对象,获取对应的SQL信息,通过StatementHandler操作JDBC的Statement对象完成与数据库的交互,然后通过ResultSetHandler处理结果集,将结果返回给调用者。

 

jdk动态代理和mybatis代理的区分:

jdk的动态代理:在实现InvocationHandler的代理类里面,需要传入一个被代理对象的实现类。

mybatis动态代理:MyBatis中通过MapperProxy类实现动态代理,只需要接口类型+方法的名称就可找到StatementID,所有不需要实现类。

 

缓存模块

缓存模块主要是在cache包下面

MyBatis通过Cache接口定义缓存对象的行为

 

MyBatis的缓存分为一级缓存和二级缓存,如果使用不当,则可能会带来数据一致性问题,下面介绍下一级缓存和二级缓存。

一级缓存

一级缓存默认是开启的,而且不能关闭。至于一级缓存为什么不能关闭,MyBatis核心开发人员做出了解释:MyBatis的一些关键特性(例如通过<association>和<collection>建立级联映射、避免循环引用(circular references)、加速重复嵌套查询等)都是基于MyBatis一级缓存实现的,而且MyBatis结果集映射相关代码重度依赖CacheKey,所以目前MyBatis不支持关闭一级缓存。

以下是官方原话:https://mybatis.org/mybatis-3/zh/java-api.html#sqlSessions

默认情况下,本地缓存数据的生命周期等同于整个 session 的周期。由于缓存会被用来解决循环引用问题和加快重复嵌套查询的速度,所以无法将其完全禁用。但是你可以通过设置 localCacheScope=STATEMENT 来只在语句执行时使用缓存。

注意,如果 localCacheScope 被设置为 SESSION,对于某个对象,MyBatis 将返回在本地缓存中唯一对象的引用。对返回的对象(例如 list)做出的任何修改将会影响本地缓存的内容,进而将会影响到在本次 session 中从缓存返回的值。因此,不要对 MyBatis 所返回的对象作出更改(这句话仍然不太理解),以防后患。

问:为什么mybatis一级缓存不能关闭?

源码角度来看:

ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
  if (closed) {
    throw new ExecutorException("Executor was closed.");
  }
  if (queryStack == 0 && ms.isFlushCacheRequired()) {
    clearLocalCache();
  }
  List<E> list;
  try {
    queryStack++;
    // 从缓存中获取结果
    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    if (list != null) {
      handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    } else {
      // 若缓存中获取不到,则调用queryFromDataBase方法从数据库中查询
      list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }
  } finally {
    queryStack--;
  }

从代码中看,查询时,没有任何的前置条件,默认走缓存中拿,取不到再从数据库中查询。注意查询时会判断ms.isFlushCacheRequired()是否清空缓存。因此可以配置flushCacheRequired可以在查询时,清空当前缓存。当然不建议这么操作!如下操作:将flushCache设置为true。

<select ... flushCache="false" useCache="true|false"/>

一级缓存作用域的控制:

MyBatis提供了一个配置参数localCacheScope,用于控制一级缓存的级别,该参数的取值为SESSION、STATEMENT,当指定localCacheScope参数值为SESSION时,缓存对整个SqlSession有效,只有执行DML语句(更新语句)时,缓存才会被清除。当localCacheScope值为STATEMENT时,缓存仅对当前执行的语句有效,当语句执行完毕后,缓存就会被清空。

MyBatis的一级缓存默认是SqlSession级别的缓存,在介绍MyBatis核心组件时,有提到过SqlSession提供了面向用户的API,但是真正执行SQL操作的是Executor组件。Executor采用模板方法设计模式,BaseExecutor类用于处理一些通用的逻辑,其中一级缓存相关的逻辑就是在BaseExecutor类中完成的。

一级缓存使用PerpetualCache实例实现,在BaseExecutor类中维护了两个PerpetualCache属性。其中,localCache属性用于缓存MyBatis查询结果,localOutputParameterCache属性用于缓存存储过程调用结果。

其中缓存key的生成:BaseExecutor类的createCacheKey()方法。

缓存的Key与下面这些因素有关:

(1)Mapper的Id,即Mapper命名空间与<select|update|insert|delete>标签的Id组成的全局限定名。

(2)查询结果的偏移量及查询的条数。

(3)具体的SQL语句及SQL语句中需要传递的所有参数。

(4)MyBatis主配置文件中,通过<environment>标签配置的环境信息对应的Id属性值。

 

二级缓存

默认开启,也就是说 在MyBatis主配置文件中指定cacheEnabled属性值为true 这个是可以不需要配置的。只有当<cache>标签配置了,二级缓存才能生效,只有配置了这个才会真正的使用二级缓存。

在配置Mapper文件中,通过useCache属性指定Mapper执行时是否使用缓存,这个也是默认开启的。

2.1 二级缓存的使用

官网描述:https://mybatis.org/mybatis-3/zh/sqlmap-xml.html#cache

默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。 要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:

<cache/>

基本上就是这样。这个简单语句的效果如下:

映射语句文件中的所有 select 语句的结果将会被缓存。

映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。

缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。

缓存不会定时进行刷新(也就是说,没有刷新间隔)。

缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。

缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

提示 缓存只作用于 cache 标签所在的映射文件中的语句。如果你混合使用 Java API 和 XML 映射文件,在共用接口中的语句将不会被默认缓存。你需要使用 @CacheNamespaceRef 注解指定缓存作用域。

这些属性可以通过 cache 元素的属性来修改。比如:

<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>

这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。

可用的清除策略有:

LRU – 最近最少使用:移除最长时间不被使用的对象。

FIFO – 先进先出:按对象进入缓存的顺序来移除它们。

SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。

WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。

默认的清除策略是 LRU。

flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。

size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。

readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。

提示 二级缓存是事务性的。这意味着,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/delete/update 语句时,缓存会获得更新。

 

大致原理:mybatis用了一个装饰器类CachingExecutor。如果启用了二级缓存(默认开启),mybatis在创建Executor进行装饰。只要 cacheEnabled=true 基本执行器就会被装饰。有没有配置<cache/>标签,决定了在启动的时候会不会创建这个 mapper 的 Cache 对象,最终会影响到 CachingExecutor query 方法里面的判断,如下图所示:

 

 

2.2 二级缓存源码分析

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, boundSql);
      //从二级缓存中查询是否存在key
      @SuppressWarnings("unchecked")
      List<E> list = (List<E>) tcm.getObject(cache, key);
      if (list == null) {
        list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        //此时put提交,需要在sqlSession.close,才会commit
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
    }
  }
  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

 

我们可以在单个statement id上显形关闭二级缓存(默认userCache是true):

<select id="getUser" resultType="org.entity.User" useCache="false">

二级缓存的全局关闭:

官网描述:

cacheEnabled | 全局性地开启或关闭所有映射器配置文件中已配置的任何缓存

链接地址:https://mybatis.org/mybatis-3/zh/configuration.html#settings

 

2.3 二级缓存回收策略

 1、LRU:最近最少使用的策略,移除最长时间不被使用的对象。

 2、FIFO:先进先出策略,按对象进入缓存的顺序来移除它们。

 3、SOFT:软引用策略,移除基于垃圾回收器状态和软引用规则的对象。

 4、WEAK:弱引用策略,更积极地移除基于垃圾收集器状态和弱引用规则的对象。

 

 注:软引用与弱引用的区别:

  (1)软引用: 软引用是用来描述一些有用但并不是必需的对象, 对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象

  (2)弱引用: 弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象

 

联想一下:redis-缓存失效三种策略(FIFO 、LRU、LFU)

当缓存需要被清理时(比如空间占用已经接近临界值了),需要使用某种淘汰算法来决定清理掉哪些数据。常用的淘汰算法有下面几种:

FIFO:First In First Out,先进先出。判断被存储的时间,离目前最远的数据优先被淘汰。

LRU:Least Recently Used,最近最少使用。判断最近被使用的时间,目前最远的数据优先被淘汰。

LFU:Least Frequently Used,最不经常使用。在一段时间内,数据被使用次数最少的,优先被淘汰。

 

在实际使用过程中,我们使用mybatis二级缓存的场景比较少,可以做一个大致的了解。

 

mybatis源码分析:

1、通过SqlSessionFactoryBuilder 使用XMLConfigBuilder解析配置文件

2、将解析的数据库放入Configuration对象中

3、通过Configuration获取到DefaultSqlSessionFactory(属性里面包含Executor)

4、再获取SqlSession(Executor底层封装增删改查的方法,自带一级缓存,底层HashMap)

5、创建session实例.openSession()

5.1 创建事务管理器

5.2、默认创建SimpleExecutor执行器 ,将简单执行器丢给缓存执行器CachingExecutor(通过 构造函数传递。由于默认开启了二级缓存,如果没有二级缓存配置,仍然执行的SimpleExecutor)最后丢给DefaultSqlSession。

6、通过代理设计模式获取到MapperProxy,执行目标方法(JDK动态代理)

 

整体流程如下图所示:

 

 

总结:

一级缓存是在Executor中实现的。MyBatis的Executor组件有3种不同的实现,分别为SimpleExecutor、ReuseExecutor和BatchExecutor。这些类都继承自BaseExecutor,在BaseExecutor类的query()方法中,首先从缓存中获取查询结果,如果获取不到,则从数据库中查询结果,然后将查询结果缓存起来。而MyBatis的二级缓存则是通过装饰器模式实现的,当配置cache标签开启了二级缓存,MyBatis框架会使用CachingExecutor对SimpleExecutor、ReuseExecutor或者BatchExecutor进行装饰,当执行查询操作时,对查询结果进行缓存,执行更新操作时则更新二级缓存。

 

思考:

对于mybatis缓存实现这一块,首先会使用CachingExecutor对SimpleExecutor、ReuseExecutor或者BatchExecutor进行装饰。

按照我们正常逻辑实现,对于数据的查询,首先查询一级缓存,如果没有,查询二级缓存,二级缓存没有去查询数据库,最后将查询结果缓存到一二级缓存中。

但是在mybatis中的实现,对于一级缓存的操作,不设置开关,直接去查询一级缓存。有些为了要解决一级缓存的问题,mybatis提供了flushCache会判断查询之前是否需要清除缓存,然后在查询结束时判断缓存作用域如果是statement则清空一级缓存。对于设置为statement级别的来说,实现起来就有点冗余。(当然只是笔者自己观点,可能是见识浅薄)。

当然,mybatis和spring结合起来使用时,一个事物绑定一个sqlsession。相当于mybatis一级缓存就是相当于存在ThreadLocal的数据一样,只在当前事物有效,在分布式环境中当前一些简单事物存在几十毫秒的缓存脏读也是能接受的。

 

通过文章的解读,我们要避免多个线程操作同一个session。

 

文章的最后,对于官方提及的一级缓存用于解决循环引用问题和加快重复嵌套查询的速度,这部分有兴趣的同学可以看看代码,具体在什么场景以及使用到解决这部分问题的代码位置。

 

相关书籍:

MyBatis技术内幕、MyBatis 3源码深度解析

MyBatis官网链接:https://mybatis.org/mybatis-3/zh/index.html

 

 

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值