万字长文,mybatis缓存体系详解(下)

二级缓存

主要内容:redis

图片

二级缓存构建在一级缓存之上,在收到查询请求时,MyBatis 首先会查询二级缓存。若二级缓存未命中,再去查询一级缓存。与一级缓存不一样,二级缓存和具体的命名空间绑定,一级缓存则是和 SqlSession 绑定。算法

在按照 MyBatis 规范使用 SqlSession 的状况下,一级缓存不存在并发问题。二级缓存则否则,二级缓存可在多个命名空间间共享。这种状况下,会存在并发问题,所以须要针对性去处理。除了并发问题,二级缓存还存在事务问题。sql

二级缓存如何开启?

配置项数据库

<configuration>
  <settings>
    <setting name="cacheEnabled" value="true|false" />
  </settings>
</configuration>

cacheEnabled=true表示二级缓存可用,可是要开启话,须要在Mapper.xml内配置。缓存

<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
或者 简单方式
<cache/>

对配置项属性说明:mybatis

  • flushInterval="60000",间隔60秒清空缓存,这个间隔60秒,是被动触发的,而不是定时器轮询的。
  • size=512,表示队列最大512个长度,大于则移除队列最前面的元素,这里的长度指的是CacheKey的个数,默认为1024。
  • readOnly="true",表示任何获取对象的操做,都将返回同一实例对象。若是readOnly="false",则每次返回该对象的拷贝对象,简单说就是序列化复制一份返回。
  • eviction:缓存会使用默认的Least Recently Used(LRU,最近最少使用的)算法来收回。FIFO:First In First Out先进先出队列。

在Configuration类的newExecutor方法中是否开启二级缓存并发

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
      //是否开启二级缓存
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

二级缓存经过CachingExecutor来实现的,原理是缓存里存在,就返回,不存在就调用Executor ,若是一级缓存未关闭,则先查一级缓存,不存在,再到数据库中查询。app

下面使用一张图来表示:ide

图片

下面是源码:ui

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 得到 BoundSql 对象
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // 建立 CacheKey 对象
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    // 查询
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
            throws SQLException 
{
    // 调用 MappedStatement#getCache() 方法,得到 Cache 对象,
    //即当前 MappedStatement 对象的二级缓存。
    Cache cache = ms.getCache();
    if (cache != null) { // <2> 
        // 若是须要清空缓存,则进行清空
        flushCacheIfRequired(ms);
        //当 MappedStatement#isUseCache() 方法,返回 true 时,才使用二级缓存。默认开启。   
        //可经过@Options(useCache = false) 或 <select useCache="false"> 方法,关闭。
        if (ms.isUseCache() && resultHandler == null) { // <2.2>
            // 暂时忽略,存储过程相关
            ensureNoOutParams(ms, boundSql);
            @SuppressWarnings("unchecked")
            //从二级缓存中,获取结果
            List<E> list = (List<E>) tcm.getObject(cache, key);
            if (list == null) {
                // 若是不存在,则从数据库中查询
                list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                // 缓存结果到二级缓存中
                tcm.putObject(cache, key, list); // issue #578 and #116
            }
            // 若是存在,则直接返回结果
            return list;
        }
    }
    // 不使用缓存,则从数据库中查询
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

二级缓存key是如何生成的?

也是使用的是BaseExecutor类中的createCacheKey方法生成的,因此二级缓存key和一级缓存生成规则是同样的。

二级缓存范围

二级缓存有一个很是重要的空间划分策略:

namespace="com.tian.mybatis.mappers.UserMapper"

namespace="com.tian.mybatis.mappers.RoleMapper"

即,按照namespace划分,同一个namespace,同一个Cache空间,不一样的namespace,不一样的Cache空间。

好比:

图片

在这个namespace下的二级缓存是同一个。

二级缓存何时会被清空?

每当执行insert、update、delete,flushCache=true时,二级缓存都会被清空。

事务不提交,二级缓存不生效?

SqlSession sqlSession = sqlSessionFactory.openSession();
System.out.println("第一次查询"); 
User user = sqlSession.selectOne("com.tian.mybatis.mapper.UserMapper.selectById"1);
System.out.println(user);
    
//sqlSession.commit();
                
SqlSession  sqlSession1 = sqlSessionFactory.openSession();
System.out.println("第二次查询");
User  user2 = sqlSession1.selectOne("com.tian.mybatis.mapper.UserMapper.selectById"1);
System.out.println(user2);

由于二级缓存使用的是TransactionalCaheManager(tcm)来管理的,最后又调用了TranscatinalCache的getObject()、putObject()、commit方法。

TransactionalCache里面又持有真正的Cache对象,好比:通过层层装饰的PrepetualCache。

在putObject的时候,只是添加到entriesToAddOnCommit里面。

//TransactionalCache类中 
@Override
public void putObject(Object key, Object object) {
    // 暂存 KV 到 entriesToAddOnCommit 中
    entriesToAddOnCommit.put(key, object);
}

只有conmit方法被调用的时候,才会调用flushPendingEntries方法,真正写入到缓存里。DefaultSqlSession调用commit方法的时候就会调到这个commit方法。

//TransactionalCache类中   
public void commit() {
    //若是 clearOnCommit 为 true ,则清空 delegate 缓存
    if (clearOnCommit) {
      delegate.clear();
    }
    // 将 entriesToAddOnCommit、entriesMissedInCache 刷入 delegate 中
    flushPendingEntries();
    // 重置
    reset();
  }
private void flushPendingEntries() {
    // 将 entriesToAddOnCommit 刷入 delegate 中
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    // 将 entriesMissedInCache 刷入 delegate 中
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
}
private void reset() {
    // 重置 clearOnCommit 为 false
    clearOnCommit = false;
    // 清空 entriesToAddOnCommit、entriesMissedInCache
    entriesToAddOnCommit.clear();
    entriesMissedInCache.clear();
}

为何增删该操做会清空二级缓存呢?

由于在CachingExecutor的update方法中

@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
  flushCacheIfRequired(ms);
  return delegate.update(ms, parameterObject);
}
private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    // 是否须要清空缓存
    //经过 @Options(flushCache = Options.FlushCachePolicy.TRUE) 或 <select flushCache="true"> 方式,
    //开启须要清空缓存。
    if (cache != null && ms.isFlushCacheRequired()) {
        //调用 TransactionalCache#clear() 方法,清空缓存。
        //注意,此时清空的仅仅,当前事务中查询数据产生的缓存。
        //而真正的清空,在事务的提交时。这是为何呢?
        //仍是由于二级缓存是跨 Session 共享缓存,在事务还没有结束时,不能对二级缓存作任何修改。
        tcm.clear(cache);
    }
}

如何实现多个namespace的缓存共享?

关于多个namespace的缓存共享的问题,可使用来解决。

好比:

<cache-ref namespace="com.tian.mybatis.mapper.RoleMapper"

cache-ref表明引用别名的命名空间的Cache配置,两个命名空间的操做使用的是同一个Cache。在关联的表比较少或者按照业务能够对表进行分组的时候可使用。

「注意」:在这种状况下,多个mapper的操做都会引发缓存刷新,因此这里的缓存的意义已经不是很大了。

若是将第三方缓存做为二级缓存?

Mybatis除了自带的二级换之外,咱们还能够经过是想Cache接口来自定义二级缓存。

添加依赖

     <dependency>
         <groupId>org.mybatis.caches</groupId>
         <artifactId>mybatis-redis</artifactId>
         <version>1.0.0-beta2</version>
    </dependency>

redis基础配置项

   host=127.0.0.1
   port=6379
   connectionTimeOut=5000
   soTimeout=5000
   datebase=0

在咱们的UserMapper.xml中添加

<cache type="org.mybatis.caches.redis.RedisCache"
       eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>

RedisCache类图,Cache就是Mybatis中缓存的顶层接口。

图片

二级缓存应用场景

对于访问多的查询请求且用户对查询结果实时性要求不高,此时可采用mybatis二级缓存技术下降数据库访问量,提升访问速度,业务场景好比:耗时较高的统计分析sql、电话帐单查询sql等。

缓存查询顺序

先查二级缓存,不存在则坚持一级缓存是否关闭,没关闭,则再查一级缓存,还不存在,最后查询数据库。

图片

二级缓存总结

二级缓存开启方式有两步:

第一步:在全局配置中添加配置

<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>

第二步,在Mapper中添加配置

<cache type="org.mybatis.caches.redis.RedisCache"
           eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>

二级换是默认开启的,可是针对每个Mapper的二级缓存是须要手动开启的。

二级缓存的key和一级缓存的key是同样的。

每当执行insert、update、delete,flushCache=true时,二级缓存都会被清空。

咱们能够继承第三方缓存来做为Mybatis的二级缓存。

总结

本文先从总体分析了Mybatis的缓存体系结构,而后就是对每一个缓存实现类的源码进行分析,有详细的讲述一级缓存和二级缓存,如何开启关闭,缓存的范围的说明,缓存key是如何生成的,对应缓存是何时会被清空,先走二级缓存在走以及缓存,二级缓存使用第三方缓存。

参考:http://www.tianxiaobo.com/2018/08/25/

掌握Mybatis动态映射,我但是下了功夫的
《写给大忙人看的JAVA核心技术》.pdf

相关文章
相关标签/搜索