写点什么

mybatis 二级缓存,7 个问题拿下它

用户头像
田维常
关注
发布于: 2020 年 12 月 19 日

二级缓存


主要内容:


图片


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


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


二级缓存如何开启?


配置项


`<configuration>

  <settings>

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

  </settings>

</configuration>

`


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


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

或者 简单方式

<cache/>

`


对配置项属性说明:


  • 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 ,如果一级缓存未关闭,则先查一级缓存,不存在,再到数据库中查询。


下面使用一张图来表示:


图片


下面是源码:


`@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=6379connectionTimeOut=5000soTimeout=5000datebase=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/201...


推荐阅读


掌握Mybatis动态映射,我可是下了功夫的


《写给大忙人看的JAVA核心技术》.pdf


发布于: 2020 年 12 月 19 日阅读数: 29
用户头像

田维常

关注

关注公众号:Java后端技术全栈,领500G资料 2020.10.24 加入

关注公众号:Java后端技术全栈,领500G资料

评论

发布
暂无评论
mybatis二级缓存,7个问题拿下它