Mybatis一二级缓存的理解

频繁的数据库操作是非常耗费性能的(主要是因为对于DB而言,数据是持久化在磁盘中的,因此查询操作需要通过IO,IO操作速度相比内存操作速度慢了好几个量级),尤其是对于一些相同的查询语句,完全可以把查询结果存储起来,下次查询同样的内容的时候直接从内存中获取数据即可,这样在某些场景下可以大大提升查询效率。

MyBatis的缓存分为两种:

  1. 一级缓存,一级缓存是SqlSession级别的缓存,对于相同的查询,会从缓存中返回结果而不是查询数据库
  2. 二级缓存,二级缓存是Mapper级别的缓存,定义在Mapper文件的<cache>标签中并需要开启此缓存,多个Mapper文件可以共用一个缓存,依赖<cache-ref>标签配置

SqlSession>DefaultSqlSession(selectList)>this.executor.query>Executor(一级缓存BaseExecutor, 二级缓存CachingExecutor)

CacheKey判断两次查询条件是否一致。

<wiz_code_mirror>

一级缓存:/executor/BaseExecutor.class
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
        if(this.closed) {
            throw new ExecutorException("Executor was closed.");
        } else {
            if(this.queryStack == 0 && ms.isFlushCacheRequired()) {
                this.clearLocalCache();
            }
?
            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;
            }
?
            if(this.queryStack == 0) {
                Iterator i$ = this.deferredLoads.iterator();
?
                while(i$.hasNext()) {
                    BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)i$.next();
                    deferredLoad.load();
                }
?
                this.deferredLoads.clear();
                if(this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                    this.clearLocalCache();
                }
            }
?
            return list;
        }
    }

一级缓存三个结论:

  1. MyBatis的一级缓存是SqlSession级别的,但是它并不定义在SqlSessio接口的实现类DefaultSqlSession中,而是定义在DefaultSqlSession的成员变量Executor中,Executor是在openSession的时候被实例化出来的,它的默认实现为SimpleExecutor
  2. MyBatis中的一级缓存,与有没有配置无关,只要SqlSession存在,MyBastis一级缓存就存在,localCache的类型是PerpetualCache,它其实很简单,一个id属性+一个HashMap属性而已,id是一个名为"localCache"的字符串,HashMap用于存储数据,Key为CacheKey,Value为查询结果
  3. MyBatis的一级缓存查询的时候默认都是会先尝试从一级缓存中获取数据的,但是我们看第6行的代码做了一个判断,ms.isFlushCacheRequired(),即想每次查询都走DB也行,将<select>标签中的flushCache属性设置为true即可,这意味着每次查询的时候都会清理一遍PerpetualCache,PerpetualCache中没数据,自然只能走DB。增删改没有一级缓存。

<wiz_code_mirror>

 //缓存判断查询条件是否一致:
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
        if(this.closed) {
            throw new ExecutorException("Executor was closed.");
        } else {
            CacheKey cacheKey = new CacheKey();
            cacheKey.update(ms.getId());//判断id属性是否相同
            cacheKey.update(Integer.valueOf(rowBounds.getOffset()));//判断Offset属性是否相同
            cacheKey.update(Integer.valueOf(rowBounds.getLimit()));//判断Limit属性是否相同
            cacheKey.update(boundSql.getSql());//判断sql属性是否相同
            List parameterMappings = boundSql.getParameterMappings();//后面都是判断参数是否相同
            TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
?
            for(int i = 0; i < parameterMappings.size(); ++i) {
                ParameterMapping parameterMapping = (ParameterMapping)parameterMappings.get(i);
                if(parameterMapping.getMode() != ParameterMode.OUT) {
                    String propertyName = parameterMapping.getProperty();
                    Object value;
                    if(boundSql.hasAdditionalParameter(propertyName)) {
                        value = boundSql.getAdditionalParameter(propertyName);
                    } else if(parameterObject == null) {
                        value = null;
                    } else if(typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                        value = parameterObject;
                    } else {
                        MetaObject metaObject = this.configuration.newMetaObject(parameterObject);
                        value = metaObject.getValue(propertyName);
                    }
?
                    cacheKey.update(value);
                }
            }
?
            return cacheKey;
        }
    }

一级缓存从四组共五个条件判断两次查询是相同的:

  1. <select>标签所在的Mapper的Namespace+<select>标签的id属性
  2. RowBounds的offset和limit属性,RowBounds是MyBatis用于处理分页的一个类,offset默认为0,limit默认为Integer.MAX_VALUE
  3. <select>标签中定义的sql语句
  4. 输入参数的具体参数值,一个int值就update一个int,一个String值就update一个String,一个List就轮询里面的每个元素进行update

即只要两次查询满足以上三个条件且没有定义flushCache="true",那么第二次查询会直接从MyBatis一级缓存PerpetualCache中返回数据,而不会走DB。

假如定义了MyBatis二级缓存,那么MyBatis二级缓存读取优先级高于MyBatis一级缓存

MyBatis二级缓存的生命周期即整个应用的生命周期,应用不结束,定义的二级缓存都会存在在内存中。

从这个角度考虑,为了避免MyBatis二级缓存中数据量过大导致内存溢出,MyBatis在配置文件中给我们增加了很多配置例如size(缓存大小)、flushInterval(缓存清理时间间隔)、eviction(数据淘汰算法)来保证缓存中存储的数据不至于太过庞大。

<wiz_code_mirror>

二级缓存:/executor/CachingExecutor.class
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        Cache cache = ms.getCache();//Cache是从MappedStatement中获取到的,而MappedStatement又和每一个<insert>、<delete>、<update>、<select>绑定并在MyBatis启动的时候存入Configuration中:
        if(cache != null) {
            this.flushCacheIfRequired(ms);//根据flushCache=true或者flushCache=false判断是否要清理二级缓存
            if(ms.isUseCache() && resultHandler == null) {
                this.ensureNoOutParams(ms, parameterObject, boundSql);//保证MyBatis二级缓存不会存储存储过程的结果
                List list = (List)this.tcm.getObject(cache, key);//tcm装饰器模式,创建一个事物缓存TranactionalCache,持有Cache接口,Cache接口的实现类就是根据我们在Mapper文件中配置的<cache>创建的Cache实例
                if(list == null) {
                    list = this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    this.tcm.putObject(cache, key, list);
                }//如果没有从MyBatis二级缓存中拿到数据,那么就会查一次数据库,然后放到MyBatis二级缓存中去
?
                return list;
            }
        }
//优先读取以上二级缓存,query方法优先读取默认实现的一级缓存。
        return this.delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }

MyBatis支持三种类型的二级缓存:

  • MyBatis默认的缓存,type为空,Cache为PerpetualCache
  • 自定义缓存
  • 第三方缓存

开启二级缓存:

<wiz_code_mirror>

    <settings>//mybatis.cfg.xml
        <!-- 开启二级缓存  默认值为true -->
        <setting name="cacheEnabled" value="true"/>
    </settings>
    
<mapper namespace="cn.sxt.vo.user.mapper">//mapper.xml
    <!-- 开启本mapper namespace下的二级缓存 -->
    <cache></cache>

存在问题:

select a.col1, a.col2, a.col3, b.col1, b.col2, b.col3 from tableA a, tableB b where a.id = b.id;

对于tableA与tableB的操作定义在两个Mapper中,分别叫做MapperA与MapperB,即它们属于两个命名空间,如果此时启用缓存:

  1. MapperA中执行上述sql语句查询这6个字段
  2. tableB更新了col1与col2两个字段
  3. MapperA再次执行上述sql语句查询这6个字段(前提是没有执行过任何insert、delete、update操作)

此时问题就来了,即使第(2)步tableB更新了col1与col2两个字段,第(3)步MapperA走二级缓存查询到的这6个字段依然是原来的这6个字段的值,因为我们从CacheKey的3组条件来看:

  1. <select>标签所在的Mapper的Namespace+<select>标签的id属性
  2. RowBounds的offset和limit属性,RowBounds是MyBatis用于处理分页的一个类,offset默认为0,limit默认为Integer.MAX_VALUE
  3. <select>标签中定义的sql语句

对于MapperA来说,其中的任何一个条件都没有变化,自然会将原结果返回。

这个问题对于MyBatis的二级缓存来说是一个无解的问题,因此使用MyBatis二级缓存有一个前提:

必须保证所有的增删改查都在同一个命名空间下才行

原文地址:https://www.cnblogs.com/shuchen007/p/9193190.html

时间: 2024-07-31 06:42:10

Mybatis一二级缓存的理解的相关文章

mybatis 源码分析(四)一二级缓存分析

本篇博客主要讲了 mybatis 一二级缓存的构成,以及一些容易出错地方的示例分析: 一.mybatis 缓存体系 mybatis 的一二级缓存体系大致如下: 首先当一二级缓存同时开启的时候,首先命中二级缓存: 一级缓存位于 BaseExecutor 中不能关闭,但是可以指定范围 STATEMENT.SESSION: 整个二级缓存虽然经过了很多事务相关的组件,但是最终是落地在 MapperStatement 的 Cache 中(Cache 的具体实例类型可以在 mapper xml 的 cach

深入了解MyBatis二级缓存

深入了解MyBatis二级缓存 标签: mybatis二级缓存 2015-03-30 08:57 41446人阅读 评论(13) 收藏 举报  分类: Mybatis(51)  版权声明:版权归博主所有,转载请带上本文链接!联系方式:[email protected] 目录(?)[+] 深入了解MyBatis二级缓存 一.创建Cache的完整过程 我们从SqlSessionFactoryBuilder解析mybatis-config.xml配置文件开始: Reader reader = Reso

mybatis学习--缓存(一级和二级缓存)

声明:学习摘要! MyBatis缓存 我们知道,频繁的数据库操作是非常耗费性能的(主要是因为对于DB而言,数据是持久化在磁盘中的,因此查询操作需要通过IO,IO操作速度相比内存操作速度慢了好几个量级),尤其是对于一些相同的查询语句,完全可以把查询结果存储起来,下次查询同样的内容的时候直接从内存中获取数据即可,这样在某些场景下可以大大提升查询效率. MyBatis的缓存分为两种: 一级缓存,一级缓存是SqlSession级别的缓存,对于相同的查询,会从缓存中返回结果而不是查询数据库 二级缓存,二级

MyBatis 一级缓存和二级缓存及ehcache整合

一级缓存 什么是缓存?? 缓存是存储在内存(cache)中的数据,一般情况都存在内存,在内存数据存储满了,会存储到硬盘上(disk),或是在我们进行一些操作和配置也可以把缓存存储到磁盘中. 缓存的作用是什么?? 缓存的作用可以减轻数据库的压力,减少用户对数据库的访问,可以说用户对数据库进行的重复操作在缓存中就可以实现操作,提高用户体验. 下面这张图是缓存的理解图 曾删改会对缓存造成影响. 写个测试,测试一下缓存是否存在:   答案是肯定的 现在测试一下进行曾删改数据,是否会对缓存造成影响? 二级

mybatis查询缓存——(十三)

1.     mybatis缓存介绍 如下图,是mybatis一级缓存和二级缓存的区别图解: mybatis提供查询缓存,用于减轻数据压力,提高数据库性能. mybaits提供一级缓存,和二级缓存. 一级缓存是SqlSession级别的缓存.在操作数据库时需要构造 sqlSession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据.不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的. 二级缓存是mapper级别的缓存,多个SqlSession去操作同一个

MyBatis一级缓存、二级缓存

一.MyBatis一级缓存 MyBatis默认启动一级缓存,一级缓存是SqlSession级别的 注意:有两个因素会使一级缓存失效:  1.对SqlSession进行commit()操作(即对数据库进行了增.删.改操作).数据库中的数据发生了改变,此时若再从内存中读取缓存的数据,则会读取到错误的数据信息,所以此时旧的一级缓存中的数据会清空,当用户下一次执行查询操作时, 会重新从数据库中读取数据并放入一级缓存中  2.关闭SqlSession.一级缓存的设计是每个sqlsession单独使用一个缓

MyBatis 一级缓存与二级缓存

MyBatis一级缓存 MyBatis一级缓存默认开启,一级缓存为Session级别的缓存,在执行以下操作时一级缓存会清空 1.执行session.clearCache(); 2.执行CUD操作 3.session.close(); //不是同一个Session对象了 MyBatis二级缓存 需要配置<cache></cache> 是一个映射文件级的缓存 使用Mybatis二级缓存时查询的对象实体类必须序列化实现(实现Serializable接口) 二级缓存使用时 必须使用sess

Spring+SpringMVC+MyBatis深入学习及搭建(八)——MyBatis查询缓存

1.什么是查询缓存 mybatis提供查询缓存,用于减轻数据库压力,提高数据库性能. mybatis提供一级缓存和二级缓存. 一级缓存是SqlSession级别的缓存.在操作数据库时需要构造sqlSession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据.不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的. 二级缓存是mapper级别的缓存,多个sqlSession去操作同一个Mapper的sql语句,多个sqlSession可以共用二级缓存,二级缓存

MyBatis一级缓存引起的无穷递归

MyBatis一级缓存引起的无穷递归 引言: 最近在项目中参与了一个领取优惠劵的活动,当多个用户领取同一张优惠劵的时候,使用了数据库锁控制并发,起初的设想是:如果多个人同时领一张劵,第一个到达的人领取成功,其它的人继续查找是否还有剩余的劵,如果有,继续领取,否则领取失败.在实现中,我一开始使用了递归的方式去查找劵,实际的测试中发现出现了无穷递归,通过degug和查阅资料才发现这是由于mybatis的一级缓存引起的,以下将这次遇到的问题和大家分享讨论. 1.涉及到的知识点 Mybatis缓存: 一