文章目录

  • 一、前言
  • 二、一级缓存
  • 1. 简单使用
  • 2. 问题:当MyBatis单独使用时,一级缓存默认生效,但是当和Spring整合就即失效?
  • 3. MyBatis 事务管理模式:
  • 三、二级缓存

一、前言

在实际项目开发中,通常对数据库查询的性能要求很高,而Mybatis提供了查询缓存数据,从而达到提高查询性能的要求。MyBatis的查询缓存分为一级缓存和二级缓存。一级缓存是SqlSessin级别的缓存,二级缓存是mapper 级别的缓存,二级缓存是多个SqlSession共享的。需要注意,MyBatis的缓存机制是基于id 进行缓存的,也就是说MyBatis使用HashMap缓存数据时,实际用对象的id作为key,而对象作为value保存的。
SqlSessionFactory : SqlSessionFactory 是Mybatis的关键对象,他是单个数据库映射关系经过编译后端内存镜像。SqlSessionFactory对象的实例可以通过SqlSessionFactoryBuilder对象从XML配置文件或一个预先定制的Configuration的实例来构建。SqlSessionFactory是线程安全的。
SqlSession : SqlSession 是Mybatis中持久化操作的对象,类似于JDBC中的Connection。他是应用程序和持久存储层之间执行交互操作的一个单线程对象。SqlSeesion是线程不安全的,所以不能被共享,每个线程都应有他自己的是SqlSession实例(Spring中在ThreadLocal中便保存着SqlSession副本)。

二、一级缓存

1. 简单使用

一级缓存的作用域是SqlSession范围的。当在同一个SqlSession中执行两次相同的sql语句时,第一次执行完毕会将数据库中查询的数据写到缓存(内存)中,第二次查询时会从缓存中获取数据,不再去底层数据库查询。如果SqlSession执行的DML操作,即插入、更新、删除操作,并提交到数据库。MyBatis会清空SqlSession中的一级缓存,以保证缓存中的信息时最新的。
MyBatis默认开启一级缓存。

可在Mybatis的配置文件中通过 localCacheScope 进行缓存作用域的配置

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <!--使用jdbc的getGeneratekeys获取自增主键值-->
        <setting name="useGeneratedKeys" value="true"/>
        <!--使用列别名替换别名  默认true-->
        <setting name="useColumnLabel" value="true"/>
        <!--开启驼峰命名转换Table:create_time到 Entity(createTime)-->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!-- 打印日志 -->
        <setting name="logImpl" value="STDOUT_LOGGING" />

        <!--
            localCacheScope 取值有SESSION 和 STATEMENT。默认SESSION。
            SESSION情况下,会缓存一个会话中执行的所有查询。
            STATEMENT情况下,本地会话仅用在语句执行上,对相同的SqlSession的不同调用将不会共享数据
         -->
        <setting name="localCacheScope" value="SESSION"></setting>
        <!--<setting name="lazyLoadingEnabled" value="true"></setting>-->
        <!--<setting name="aggressiveLazyLoading" value="false"></setting>-->
    </settings>
</configuration>

在Spring整合后的MyBatis,如果不开启事务,在每次操作数据库的时候都会创建一个SqlSession,在操作结束后关闭。

下面是一个简单的方法,里面进行了两次数据库查询

未启用事务下,一级缓存失效:打印两次Sql语句代表查询两次数据库

spring mybatis xml缓存_缓存


spring mybatis xml缓存_sql_02

当启用事务后,一级缓存生效 : 只查询了一次数据库,第二次查询从缓存中拿取

spring mybatis xml缓存_一级缓存_03


spring mybatis xml缓存_缓存_04

2. 问题:当MyBatis单独使用时,一级缓存默认生效,但是当和Spring整合就即失效?

调用接口后可以发现。未开启事务时,SqlSession创建了两次。同时之前也说了一级缓存的作用域是SqlSession范围的。即SqlSession关闭后,一级缓存就失效了。所以这里会查询两次,因为SqlSession的关闭导致一级缓存失效。

原因总结:

原因在于当mybatis和spring结合使用的时候,将原本的DefaultSqlSession替换成了SqlSessionTemplate,并且在SqlSessionTemplate将sqlSession替换成了代理对象,当我们对数据库进行操作时方法的时候会调用到SqlSessionInterceptor的invoke方法, 而在invoke方法的fianlly中调用了SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)将我们的session关闭了。原生的mybatis之所以没有关闭session是因为它把session暴露给我们了,而和spring结合使用的时候并没有提供暴露session的方法,所以只能在这里关,而一旦session关闭了,那一级缓存自然也就失效了。

源码分析:

通过断点发现 SqlSessionDaoSupport 中将创建

spring mybatis xml缓存_sql_05


容器将自动为给定的 SqlSessionFactory 创建SqlSessionTemplate。进入 createSqlSessionTemplate 一路前行到

SqlSessionTemplate 下面的方法构造方法

spring mybatis xml缓存_一级缓存_06


在这里可以看到,Spring 对原先的 sqlSession进行了代理,返回指定接口的代理类的实例sqlSessionProxy,并且将方法调用分派给指定的内部类 SqlSessionInterceptor 来处理。

接下来看内部类 SqlSessionInterceptor

/**
   * Proxy needed to route MyBatis method calls to the proper SqlSession got
   * from Spring's Transaction Manager
   * It also unwraps exceptions thrown by {@code Method#invoke(Object, Object...)} to
   * pass a {@code PersistenceException} to the {@code PersistenceExceptionTranslator}.
   */
  private class SqlSessionInterceptor implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    	// 在方法执行前会先创建一个SqlSession。
      SqlSession sqlSession = getSqlSession(
          SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType,
          SqlSessionTemplate.this.exceptionTranslator);
      try {
      	// 代理方法执行
        Object result = method.invoke(sqlSession, args);
        // 判断MyBatis的事务管理模式,是否是托管给Spring容器管理(后面详解)
        if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
          // force commit even on non-dirty sessions because some databases require
          // a commit/rollback before calling close()
          sqlSession.commit(true);
        }
        return result;
      } catch (Throwable t) {
        Throwable unwrapped = unwrapThrowable(t);
        if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
          // release the connection to avoid a deadlock if the translator is no loaded. See issue #22
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
          sqlSession = null;
          Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
          if (translated != null) {
            unwrapped = translated;
          }
        }
        throw unwrapped;
      } finally {
      	// 在这里可以看到如果在执行结束后会关闭SqlSession,从而导致MyBatis一级缓存失效。
        if (sqlSession != null) {
          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
        }
      }
    }
  }

getSqlSession() 方法中会调用 registerSessionHolder 方法。该方法会同步是否活动的,如果 存在活动的事务,则注册会话持有者。而Spring的事务即是活动的。所以如果使用事务会注册会话持有者。注册会话持有者时会调用 ResourceHolderSupport.requested() 方法将引用计数器 referenceCount 加1。

spring mybatis xml缓存_sql_07

在closeSqlSession方法中。如果未启动事务,则会直接调用session.close()方法关闭session。如果启用事务,则在 getSqlSession 时注册了会话持有者,所以会调用ResourceHolderSupport.released()方法将引用计数减少1,因为holder已被释放,当注册的会话持有者。并不会关闭session。

spring mybatis xml缓存_一级缓存_08


上述代码可以看出在没有启动事务的情况下,由于SqlSession关闭导致一级缓存失效的原因,启用事务时,由于SqlSession并未关闭,所以一级缓存未失效。下面继续,事务开启后何时提交和关闭的。

断点走到方法执行结束后

spring mybatis xml缓存_sql_09


下一步的断点走到了AppUtils中的 invokeJoinpointUsingReflection 方法。其实在调用方法时断点就走到了

前一句。明显可知这里使用代理为了完成AOP。

spring mybatis xml缓存_sql_10


而下一步断点后面又到了 TransactionAspectSupport 类的 invokeWithinTransaction方法。这里看类名就很明确了,这是事务支持的切面。

spring mybatis xml缓存_sql_11

即在completeTransactionAfterThrowing 处理事务回滚。在 commitTransactionAfterReturning中处理事务提交。

随后根据 txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); 方法可知,进入AbstractPlatformTransactionManager中 commit -> processCommit()方法中 triggerBeforeCommittriggerBeforeCompletion 等方法则会触发一些回调。

spring mybatis xml缓存_一级缓存_12

举例triggerBeforeCommit 方法会调用 TransactionSynchronizationUtils.triggerBeforeCommit(status.isReadOnly());

spring mybatis xml缓存_缓存_13


可以看到轮询触发回调。

spring mybatis xml缓存_sql_14


则会触发到 SqlSessionUtils 类的内部类 SqlSessionSynchronization中,执行对应的回调方法。

spring mybatis xml缓存_缓存_15


至此,整个问题流程大体分析完毕。

3. MyBatis 事务管理模式:

上面有说道 MyBatis的事务管理模式:
MyBatis的事务管理器有两种类型,分别是JDBCMANAGED

  • JDBC : 这个配置就至二级使用了JDBC的提交和回滚设置,它依赖于从数据源得到的连接来管理事务范围。
  • MANAGED:这个配置基本没做什么, 他从不提交或者回滚一个连接。而是让容器来管理实务的整个生命周期,默认情况下他会关闭连接,可以通过closeConnection 设置为false来解决问题。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
       PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
       "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
   <environments default="dev">
       <environment id="dev">
           <transactionManager type="MANAGED">
               <property name="closeConnection" value="false"></property>
           </transactionManager>
           <dataSource type="...">
               ....
           </dataSource>
       </environment>
   </environments>
</configuration>

这也是为什么上面源码分析的时候需要判断一下是否是 MANAGED模式,否则直接将事务提交的原因。

三、二级缓存

二级缓存是Mapper级别的缓存,使用二级缓存时,多个SqlSession使用同一个Mapper的sql语句去操作数据库,得到的数据会存在二级缓存区。二级缓存区同样也是使用HashMap进行存储数据。相较于一级缓存,二级缓存范围更大,且是跨SqlSession的,即多个SqlSession共享二级缓存。
MyBatis默认没有开启二级缓存,需要在配置文件中进行配置 cacheEnabled 为true

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <!--使用jdbc的getGeneratekeys获取自增主键值-->
        <setting name="useGeneratedKeys" value="true"/>
        <!--使用列别名替换别名  默认true-->
        <setting name="useColumnLabel" value="true"/>
        <!--开启驼峰命名转换Table:create_time到 Entity(createTime)-->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!-- 打印日志 -->
        <setting name="logImpl" value="STDOUT_LOGGING" />

        <!--<!–-->
            <!--localCacheScope 取值有SESSION 和 STATEMENT。默认SESSION。-->
            <!--SESSION情况下,会缓存一个会话中执行的所有查询。-->
            <!--STATEMENT情况下,本地会话仅用在语句执行上,对相同的SqlSession的不同调用将不会共享数据-->
         <!--–>-->
        <!--<setting name="localCacheScope" value="SESSION"></setting>-->

        <!-- 开启二级缓存。默认是false不开启 -->
        <setting name="cacheEnabled" value="true"></setting>

        <!--<setting name="lazyLoadingEnabled" value="true"></setting>-->
        <!--<setting name="aggressiveLazyLoading" value="false"></setting>-->
    </settings>
</configuration>

随后在Mapper类上加上注解 @CacheNamespace

@CacheNamespace
public interface UserMapper {
    List<User> selectUser(int id);
 }

看到注解开启,多次查询并再次从数据库中查询。

spring mybatis xml缓存_缓存_16


CacheNamespace 注解的属性 :

配置名

释义

eviction

回收策略,默认LRU。 共有四种 -----------> LRU:最近最少使用策略,移除最长时间不被使用的对象; FIFO先进先出策略;SOFT : 软引用策略,移除基于垃圾回收期状态和软引用规则的对象; WEAK : 弱引用策略, 更积极的移除基于垃圾回收器状态和弱引用规则的对象

flushInterval

缓存刷新间隔,单位毫秒,可为任意正整数。默认不设置,即不刷新,仅调用语句时刷新

size

缓存数据,可以为任意正整数。默认1024

readWrite

是否可读写,默认true。只读的缓存会返回给所有调用者相同的实例,因此这些对象不能被修改。 可读的缓存会返回缓存对象的拷贝(通过序列化)

implementation

缓存实现类型,默认org.apache.ibatis.cache.impl.PerpetualCache

注:

  1. 使用二级缓存时,与查询结果映射的java对象必须实现 Serializable 接口。如果存在父类,其成员都需要实现序列化接口。因为二级缓存的介质不一定是内存,可能是硬盘或远程服务器,所以需要对缓存数据实现序列化和反序列化接口。
  2. 可在Mapper.xml文件中进行 类似@CacheNamespace 注解的配置,如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.kingfish.dao.UserMapper">
    <!-- 创建一个LRU缓存,每隔60s刷新,最大存储512个对象,返回对象被认为是只读的,配置二级缓存实现类型-->
   <cache eviction="LRU" flushInterval="60000" size="512" readOnly="true" type="org.apache.ibatis.cache.impl.PerpetualCache"/>
    <!-- useCache : 是否使用二级缓存,默认是true -->
    <!-- flushCache : 是否清空二级缓存,默认是true -->
    <select id="selectUser" resultType="com.kingfish.pojo.User" useCache="true">
        select *
        from `user`;
    </select>
</mapper>
  1. useCache : 是否使用二级缓存,默认是true
    flushCache : 是否清空二级缓存,默认是true

    如:select不想被缓存时,可以添加select的属性useCache=“false”;insert、update和delete不想让他刷新缓存时,添加属性flushCache=”false ”。