Mybatis缓存详解

一级缓存

可不可以给我说说什么是一级缓存?解决什么问题的?

答: 当我们建立SqlSession时,就可以通过Mybatis进行sql查询,假如本次session查询时我们需要进行两次相同的sql查询,就需要进行进行两次的磁盘IO,为了避免这种没必要的等待,Mybatis为每一个SqlSession设置一级缓存,在同一个SqlSession中,一级缓存会将第一次查询结果缓存起来,第二次相同的查询就可以直接使用了。

如下图,第一次查询时,该SqlSession就会将executor的查询结果存到缓存中。

在这里插入图片描述

第二次查询时,由于查询内容和第一次一样,所以直接从缓存中返回结果即可。

在这里插入图片描述

举个例子演示一下怎么用一级缓存吧

答: Mybatis默认是开启一级缓存的,如下所示,可以发现只要第二次使用的sql和参数一样,就会从一级缓存中获取数据。

 User1 user1 = user1Mapper.select("1");
        logger.info("一级缓存第一次查询:[{}]", user1);

        User1 user11 = user1Mapper.select("1");
        logger.info("一级缓存第二次查询:[{}]", user11);


        User1 user12 = user1Mapper.select("2");
        logger.info("一级缓存第三次查询,id不同:[{}]", user12);
       

输出结果

 /**
         * 输出结果
         * 2022-11-27 15:51:28,313 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==>  Preparing: select * from user1 where id = ?
         * 2022-11-27 15:51:28,338 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==> Parameters: 1(String)
         * 2022-11-27 15:51:28,539 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - <==      Total: 1
         * 2022-11-27 15:51:28,541 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==>  Preparing: select * from user1 where id = ?
         * 2022-11-27 15:51:28,541 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==> Parameters: 2(String)
         * [main] INFO com.zsy.mapper.MyBatisTest - 一级缓存第一次查询:[User1{id='1', name='小明', user2=null}]
         * [main] INFO com.zsy.mapper.MyBatisTest - 一级缓存第二次查询:[User1{id='1', name='小明', user2=null}]
         * 2022-11-27 15:51:28,667 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - <==      Total: 1
         * [main] INFO com.zsy.mapper.MyBatisTest - 一级缓存第三次查询,id不同:[User1{id='2', name='小王', user2=null}]
         */

一级缓存的执行过程能不能给我说说?

答: 这个问题我们不妨以终为始来了解,通过IDEA将上文缓存的put方法中打一个断点,来了解一下执行流程。当然我们也得有一个查询代码,查询代码如下所示

User1 user1 = user1Mapper.select("1");
        logger.info("一级缓存第一次查询:[{}]", user1);

这个查询首先会触发Mybatis创建的代理对象MapperProxy的调用

 public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
            return this.mapperMethod.execute(sqlSession, args);
        }

然后走到execute,可以看到真正调用selectOne执行

public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        Object param;
        switch(this.command.getType()) {
       ........
       //更具返回类型决定select逻辑
        case SELECT:
            if (this.method.returnsVoid() && this.method.hasResultHandler()) {
                this.executeWithResultHandler(sqlSession, args);
                result = null;
            } else if (this.method.returnsMany()) {
                result = this.executeForMany(sqlSession, args);
            } else if (this.method.returnsMap()) {
                result = this.executeForMap(sqlSession, args);
            } else if (this.method.returnsCursor()) {
                result = this.executeForCursor(sqlSession, args);
            } else {
            //本次查询是查询一个对象值的所以走到selectOne
                param = this.method.convertArgsToSqlCommandParam(args);
                result = sqlSession.selectOne(this.command.getName(), param);
                if (this.method.returnsOptional() && (result == null || !this.method.getReturnType().equals(result.getClass()))) {
                    result = Optional.ofNullable(result);
                }
            }
       .......
        }

再步入可以发现selectOne逻辑说白了就是调用selectList再去第一条,若有多条数据则直接报错

public <T> T selectOne(String statement, Object parameter) {
        List<T> list = this.selectList(statement, parameter);
        if (list.size() == 1) {
            return list.get(0);
        } else if (list.size() > 1) {
            throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
        } else {
            return null;
        }
    }

不断步进可以看到selectList通过executor.query获取到执行结果var6 并返回

private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
        List var6;
        try {
            MappedStatement ms = this.configuration.getMappedStatement(statement);
            //进行真正的查询逻辑,然后直接返回
            var6 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, handler);
        } ..........

        return var6;
    }

我们看看query的逻辑吧,这个query调用者是CachingExecutor,说明这个查询会涉及缓存操作

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameterObject);
        CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);
        return this.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }

从笔者debug的图片中也能看出,boundSql 封装了查询SQL、参数等信息,然后调用query

在这里插入图片描述

不断步进我们会发现调用到BaseExecutorqueryFromDatabase,然后将查询结果存到缓存中

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        this.localCache.putObject(key, ExecutionPlaceholder.EXECUTION_PLACEHOLDER);

        List list;
        try {
        //查询逻辑
            list = this.doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } finally {
            this.localCache.removeObject(key);
        }

		//将结果缓存起来
        this.localCache.putObject(key, list);
        if (ms.getStatementType() == StatementType.CALLABLE) {
            this.localOutputParameterCache.putObject(key, parameter);
        }

        return list;
    }

最终结果就被缓存,这个key就是CacheKey key = this.createCacheKey(ms, parameterObject, rowBounds, boundSql);的结果

在这里插入图片描述

这样第二次查询时,源码就会走到BaseExecutorquery

 public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
      ........
            List list;
            try {
                ++this.queryStack;
                //查询一级缓存中是否有值,若有则直接处理返回
                list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
                if (list != null) {
                    this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
                } else {
                    list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
                }
            } finally {
                --this.queryStack;
            }

           .......

            return list;
        }
    }

总结一下流程就如下图所示

在这里插入图片描述

那它的生命周期有多长知道吗?

答: 可以从三种情况进行阐述吧:

  1. SqlSession调用了close之后,会直接释放PerpetualCache对象。缓存自然不能使用了。
  2. 进行updatedeleteinsert等操作,缓存就会被清空,但是缓存对象还能用。
  3. 调用clearCache同理,缓存被清空,但是对象还能用。

我们日常开发中如何用到一级缓存呢?

答: 日常开发过程中,我们都是整合Spring的,所以每一次查询都会创建一个新的sqlSession,所以如果希望使用一级缓存则要开启一个事务确保本次所有操作都在同一个sqlSession中。

你刚刚说一级缓存都是存在hashMap中,那会不会导致HashMap过大进而出现OOM等问题啊。

答: 不至于,一级缓存是session级别,并且随便进行一个修改操作缓存就会被清空,退一万步来说,就算对象过大,我们不也可以手动清除缓存不是吗?

二级缓存

前置代码示例

为了讨论二级缓存,我们不妨展示一个简单的二级缓存配置示例

首先Mybatis配置开启二级缓存(注意mybatis默认是开启的,这里显示声明一下而已)

 <settings>
    
        <!--开启二级缓存-->
        <setting name="cacheEnabled" value="true"/>
    </settings>

对应的Mapper.xml添加下面这段配置


    <cache/>

测试代码

  User1 user1 = user1Mapper.select("1");
        logger.info("二级缓存第一次查询:[{}]", user1);


        if (sqlSession != null) {
            sqlSession.close();
        }

        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        User1Mapper user1Mapper1 = sqlSession2.getMapper(User1Mapper.class);
        User1 user13 = user1Mapper1.select("1");
        logger.info("二级缓存第二次查询:[{}]", user13);



        if (sqlSession2 != null) {
            sqlSession2.close();
        }

可以看到使用同样的会话,第二次查询不会查询SQL而是直接从二级缓存获取数据。

2022-11-28 12:59:19,074 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@23986957]
2022-11-28 12:59:19,192 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==>  Preparing: select * from user1 where id = ?
2022-11-28 12:59:19,216 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==> Parameters: 1(String)
2022-11-28 12:59:19,339 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - <==      Total: 1
[main] INFO com.zsy.mapper.MyBatisTest - 二级缓存第一次查询:[User1{id='1', name='小明', user2=null}]
2022-11-28 12:59:19,348 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@23986957]
[main] INFO com.zsy.mapper.MyBatisTest - 二级缓存第二次查询:[User1{id='1', name='小明', user2=null}]

说说二级缓存的工作模式吧

**答:**如下图所示,在开启二级缓存配置后,框架会首先去CachingExecutor看看是否有缓存数据,若没有则会从一级缓存查询,实在找不到就通过BaseExecutor查询并处理完缓存起来。

在这里插入图片描述

注意这里CachingExecutor用到了装饰者模式,将Executor 组合进来,所以CachingExecutor会先调用(List)this.tcm.getObject(cache, key);看看缓存中是否有数据,若没有在进行进一步查询并缓存的操作。

//将基础执行器作为被装饰的成员属性组合进来
 private final Executor delegate;

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) {
            this.flushCacheIfRequired(ms);
            if (ms.isUseCache() && resultHandler == null) {
                this.ensureNoOutParams(ms, boundSql);
                //先去缓存查询
                List<E> list = (List)this.tcm.getObject(cache, key);
                if (list == null) {
                //	若为空则用非缓存执行器进行数据获取
                    list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    //将数据存到缓存中
                    this.tcm.putObject(cache, key, list);
                }

                return list;
            }
        }

        return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }

二级缓存怎么划分的知道吗?

答: 有两种,一种是自定义划分,我们在每个Mapper.xml中添加 <cache/>使得每一个mapper都有一个全局的独立缓存空间

在这里插入图片描述

假如我们希望多个mapper共享一个空间的话,需要被分享的mapper使用<cache/>,而其他mapper则用<cache-ref namespace="">指向这个空间即可。

在这里插入图片描述

使用二级缓存要具备的几个条件知道吗?

答: 总的来说是三个条件:

  1. 全局配置开启二级缓存:<setting name="cacheEnabled" value="true"/>
  2. mapper.xml标签配置了 <cache/>或者 <cache-ref/>
  3. select语句配置useCache=true

一级缓存和二级缓存的使用顺序知道吗?

答: 前面说了,先去二级缓存查,若没有再去一级缓存,最后使用执行器查SQL。

二级缓存实现的选择有哪些知道吗?

答: 有三种吧:

  1. 框架自身提供了很多缓存方案,这些缓存还提供了不同的回收策略:例如LRUFIFO等。
  2. 用户继承接口org.apache.ibatis.cache.Cache自行实现一个缓存。
  3. 通过第三方缓存工具集成。

Mybatis自身提供的二级缓存的实现了解嘛?

答: 如下图,可以看到框架自身基于装饰者模式实现了很多缓存工具,并且每个缓存容量都有限制,不同的缓存工具内存回收策略是不同的:例如LruCache即最近最少使用算法,内存容量满了就回收到现在为止最不常用的。而FifoCache同理,内存满了之后回收最先被缓存的数据,ScheduledCache则是定时清理缓存了。

在这里插入图片描述

能不能说说二级缓存关联刷新问题以及解决方案呢?

答: 二级缓存关联刷新问题说白了就是一张表有涉及多表联查然后对数据进行缓存,然后被关联表修改后,未能及时更新导致的问题。
如下图所示,UserMapper查询数据需要关联机构表,第一次查询后将数据缓存起来,在第二次查询前,机构的mapper将数据更新,这就导致UserMapper第二次查询拿到的机构信息是老的,进而导致数据一致性问题。

在这里插入图片描述

解决方案也很简单,我们只要确保缓存更新被关联表时,及时刷新响应缓存即可,具体可以参考这篇文章

MyBatis 二级缓存 关联刷新实现

二级缓存的配置参数了解嘛?

答: 主要参数有这么四个:

  1. 缓存回收策略(eviction):这个参数有这么4个LRU最近最少回收算法这种是默认的算法、FIFO先进先出算法、SOFT算法(基于垃圾回收器算法和软引用回收的对象)、WEAK算法即基于垃圾回收器算法和弱引用规则回收对象。
  2. 刷新间隔(flushInterval):单位毫秒。
  3. 容量(size):引用数目,正整数。
  4. 是否只读(readOnly):如果只读则直接返回缓存实例,性能上会相对有些优势。若不为只读则会通过序列化获取对象的拷贝,性能就相对差一些。

配置范例如下所示:

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

再问一句,二级缓存失效场景了解嘛?

**答:**有两种情况一种是第一次查询的sqlsession没有提交或者关闭

 User1 user1 = user1Mapper.select("1");
        logger.info("二级缓存第一次查询:[{}]", user1);




        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        User1Mapper user1Mapper1 = sqlSession2.getMapper(User1Mapper.class);
        User1 user13 = user1Mapper1.select("1");
        logger.info("二级缓存第二次查询:[{}]", user13);

输出结果

2022-11-29 01:05:43,339 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==>  Preparing: select * from user1 where id = ?
2022-11-29 01:05:43,363 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==> Parameters: 1(String)
2022-11-29 01:05:43,502 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - <==      Total: 1
2022-11-29 01:05:43,506 [main] DEBUG [com.zsy.mapper.User1Mapper] - Cache Hit Ratio [com.zsy.mapper.User1Mapper]: 0.0
2022-11-29 01:05:43,506 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Opening JDBC Connection
[main] INFO com.zsy.mapper.MyBatisTest - 二级缓存第一次查询:[User1{id='1', name='小明', user2=null}]
2022-11-29 01:05:44,234 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Created connection 550668305.
2022-11-29 01:05:44,234 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@20d28811]
2022-11-29 01:05:44,351 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==>  Preparing: select * from user1 where id = ?
2022-11-29 01:05:44,351 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==> Parameters: 1(String)
2022-11-29 01:05:44,465 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - <==      Total: 1
[main] INFO com.zsy.mapper.MyBatisTest - 二级缓存第二次查询:[User1{id='1', name='小明', user2=null}]

第二种则是更新操作

2022-11-29 01:07:22,302 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==>  Preparing: select * from user1 where id = ?
2022-11-29 01:07:22,326 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==> Parameters: 1(String)
2022-11-29 01:07:22,456 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - <==      Total: 1
[main] INFO com.zsy.mapper.MyBatisTest - 二级缓存第一次查询:[User1{id='1', name='小明', user2=null}]
2022-11-29 01:07:22,479 [main] DEBUG [com.zsy.mapper.User1Mapper.updatebySet] - ==>  Preparing: update user1 SET id=?, name=? where id=?
2022-11-29 01:07:22,479 [main] DEBUG [com.zsy.mapper.User1Mapper.updatebySet] - ==> Parameters: 1(String), aa(String), 1(String)
2022-11-29 01:07:22,713 [main] DEBUG [com.zsy.mapper.User1Mapper.updatebySet] - <==    Updates: 1
2022-11-29 01:07:22,714 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Rolling back JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@f8c1ddd]
2022-11-29 01:07:22,833 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@f8c1ddd]
2022-11-29 01:07:22,949 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@f8c1ddd]
2022-11-29 01:07:22,949 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Returned connection 260840925 to pool.
2022-11-29 01:07:22,949 [main] DEBUG [com.zsy.mapper.User1Mapper] - Cache Hit Ratio [com.zsy.mapper.User1Mapper]: 0.0
2022-11-29 01:07:22,949 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Opening JDBC Connection
2022-11-29 01:07:22,949 [main] DEBUG [org.apache.ibatis.datasource.pooled.PooledDataSource] - Checked out connection 260840925 from pool.
2022-11-29 01:07:22,949 [main] DEBUG [org.apache.ibatis.transaction.jdbc.JdbcTransaction] - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@f8c1ddd]
2022-11-29 01:07:23,065 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==>  Preparing: select * from user1 where id = ?
2022-11-29 01:07:23,065 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - ==> Parameters: 1(String)
[main] INFO com.zsy.mapper.MyBatisTest - 二级缓存第二次查询:[User1{id='1', name='小明', user2=null}]
2022-11-29 01:07:23,184 [main] DEBUG [com.zsy.mapper.User1Mapper.select] - <==      Total: 1

要想真正用上二级缓存,需要像这样及时提交或者关闭其他session

User1 user1 = user1Mapper.select("1");
        logger.info("二级缓存第一次查询:[{}]", user1);


        if (sqlSession != null) {
            sqlSession.close();
        }

        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        User1Mapper user1Mapper1 = sqlSession2.getMapper(User1Mapper.class);
        User1 user13 = user1Mapper1.select("1");
        logger.info("二级缓存第二次查询:[{}]", user13);



        if (sqlSession2 != null) {
            sqlSession2.close();
        }

说说Mybatis一级缓存和二级缓存的区别吧?(重点)

答:
一级缓存默认开启,作用域session,当session调用close或者flush时就会被清空,缓存也是PerpetualCache 一种基于HashMap实现的缓存。
而二级缓存作用于mapper(namespace),也是基于缓存也是PerpetualCache ,默认不开启,需要缓存的属性类必须实现序列化接口,而且二级缓存可以自定义缓存存储源。

参考文献

Mybatis进阶使用-一级缓存与二级缓存

Mybatis(六)一级缓存和二级缓存

MyBatis详解 - 一级缓存实现机制

MyBatis详解 - 二级缓存实现机制

【Java教程】看懂这篇文章-你就懂了Mybatis的二级缓存

MyBatis 二级缓存 关联刷新实现

https://github.com/shark-ctrl/toBeBetterJavaer

MyBatis 缓存配置 之 二级缓存

面渣逆袭(MyBatis面试题八股文)必看

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

shark-chili

您的鼓励将是我创作的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值