mybatis源码学习:一级缓存和二级缓存分析

mybatis源码学习:一级缓存和二级缓存分析,第1张

概述[toc] 前文传送门: "mybatis源码学习:从SqlSessionFactory到代理对象的生成" 零、一级缓存二级缓存的流程 以这里的查询语句为例。 一级缓存总结 以下

目录零、一级缓存和二级缓存的流程一级缓存总结二级缓存总结一、缓存接口Cache及其实现类二、cache标签解析源码三、CacheKey缓存项的key四、二级缓存TransactionCache五、二级缓存测试六、一级缓存源码解析七、测试一级缓存

前文传送门:mybatis源码学习:从SqlSessionFactory到代理对象的生成

零、一级缓存和二级缓存的流程

以这里的查询语句为例。

一级缓存总结

以下两种情况会直接在一级缓存中查找数据

主配置文件或映射文件没有配置二级缓存开启。二级缓存中不存在数据。

根据statetment,生成一个CacheKey。

判断是否需要清空本地缓存。

根据cachekey从localCache中获取数据。

如果缓存未命中,走接下来三步并向下

从数据库查询结果。将cachekey:数据存入localcache中。将数据返回。

如果缓存命中,直接从缓存中获取数据。

localCache的范围如果为statement,清空一级缓存。

二级缓存总结

判断主配置文件是否设置了enabledCache,默认是开启的,创建CachingExecutor。

根据statetment,生成一个CacheKey。

判断映射文件中是否有cache标签,如果没有则跳过以下针对二级缓存的 *** 作,从一级缓存中查,查不到就从数据库中查。

否则即开启了二级缓存,获取cache。

判断是否需要清空二级缓存。

判断该语句是否需要使用二级缓存isUserCache。

如果二级缓存命中,则直接返回该数据。

如果二级缓存未命中,则将cachekey存入未命中set,然后进行一下的 *** 作:

从一级缓存中查,如果命中就返回,没有命中就从数据库中查。将查到的数据返回,并将cachekey和数据(对象的拷贝)存入待加入二级缓存的map中。

最后commit和close *** 作都会使二级缓存真正地更新。

一、缓存接口Cache及其实现类

缓存类的顶级接口Cache,里面定义了加入数据到缓存,从缓存中获取数据,清楚缓存等 *** 作,通常mybatis会将namespace作为ID,将CacheKey作为Map中的键,而map中的值也就是存储在缓存中的对象。

而通过装饰器设计模式,将Cache的功能进行加强,在它的实现类中有着明显的体现:

PerpetualCache:是最基础的缓存类,采用HashMap实现,同时一级缓存使用的localCache就是该类型。

LruCache:Lru(least recently used),采用Lru算法可以实现移除最长时间没有使用的key/value。

SerializedCache:提供了序列化功能,将值序列化后存入缓存,用于缓存返回一份实例的copy,保证线程安全。

LoggingCache:提供日志功能,如果开启deBUGEnabled为true,则打印缓存命中日志。

SynchronizedCache:同步的Cache,用synchronized关键字修饰所有方法。

下图可以得知其执行链:SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache

二、cache标签解析源码

XMLMapperBuilder中的configurationElement负责解析mappers映射文件中的标签元素,其中有个cacheElement方法,负责解析cache标签。

  private voID cacheElement(XNode context) throws Exception {    if (context != null) {      //获取type属性,默认为perpetual      String type = context.getStringAttribute("type","PERPETUAL");      //获取type类对象      Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);      //获取eviction策略,默认为lru,即最近最少使用,移除最长时间不被使用的对象      String eviction = context.getStringAttribute("eviction","LRU");      Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);      //获取flushInterval刷新间隔      Long flushInterval = context.getLongAttribute("flushInterval");      //获取size引用数目      Integer size = context.getIntAttribute("size");      //获取是否只读      boolean reaDWrite = !context.getBooleanAttribute("Readonly",false);      //获取是否blocking      boolean blocking = context.getBooleanAttribute("blocking",false);      //这一步是另外一种设置cache的方式,即cache子元素中用property,name,value定义      PropertIEs props = context.getChildrenAsPropertIEs();      builderAssistant.useNewCache(typeClass,evictionClass,flushInterval,size,reaDWrite,blocking,props);    }  }

getStringAttribute方法,这个方法的作用就是获取指定的属性值,如果没有设置的话,就采用默认的值:

  public String getStringAttribute(String name,String def) {    //获取name参数对应的属性    String value = attributes.getProperty(name);    if (value == null) {      //如果没有设置,默认为def      return def;    } else {      return value;    }  }

resolveAlias方法,从源码中我们就可以猜测,我们之前通过</typeAliases>起别名其实也就是将里面的内容解析,并存入map之中,而每次处理类型的时候,都比较的是小写的形式,这也是我们起别名之后不用关心大小写的原因。

  // throws class cast exception as well if types cannot be assigned  public <T> Class<T> resolveAlias(String string) {    try {      if (string == null) {        return null;      }      //首先将传入的参数转换为小写形式      String key = string.tolowerCase(Locale.ENGliSH);      Class<T> value;      //到TypeAliasRegistry维护的Map,TYPE_AliASES中找有无对应的键      if (TYPE_AliASES.containsKey(key)) {        //找到就直接返回:class类对象        value = (Class<T>) TYPE_AliASES.get(key);      } else {        //找不到就通过反射获取一个        value = (Class<T>) Resources.classForname(string);      }      return value;    } catch (ClassNotFoundException e) {      throw new TypeException("Could not resolve type alias '" + string + "'.  Cause: " + e,e);    }  }

根据获取的属性,通过装饰器模式,层层装饰,最后创建了一个SynchronizedCache,并添加到configuration中。因此我们可以知道,一旦我们在映射文件中设置了<cache>,就会创建一个SynchronizedCache缓存对象。

  public Cache useNewCache(Class<? extends Cache> typeClass,Class<? extends Cache> evictionClass,Long flushInterval,Integer size,boolean reaDWrite,boolean blocking,PropertIEs props) {    //把当前的namespace当作缓存的ID    Cache cache = new CacheBuilder(currentnamespace)        .implementation(valueOrDefault(typeClass,PerpetualCache.class))        .addDecorator(valueOrDefault(evictionClass,LruCache.class))        .clearInterval(flushInterval)        .size(size)        .reaDWrite(reaDWrite)        .blocking(blocking)        .propertIEs(props)        .build();    //将cache加入configuration    configuration.addCache(cache);    currentCache = cache;    return cache;  }
三、CacheKey缓存项的key

默认情况下,enabledCache的全局设置是开启的,所以Executor会创建一个CachingExecutor,以查询为例,当执行Executor实现类的时候,会获取boundsql,并根据当前信息创建缓存项的key。

  @OverrIDe  public <E> List<E> query(MappedStatement ms,Object parameterObject,RowBounds rowBounds,ResultHandler resultHandler) throws sqlException {    //从MappedStatement中获取boundsql    Boundsql boundsql = ms.getBoundsql(parameterObject);    //Cachekey类表示缓存项的key    CacheKey key = createCacheKey(ms,parameterObject,rowBounds,boundsql);    return query(ms,resultHandler,key,boundsql);  }  

每一个sqlSession中持有了自己的Executor,每一个Executor中有一个Local Cache。当用户发起查询时,Mybatis会根据当前执行的MappedStatement生成一个key,去Local Cache中查询,如果缓存命中的话,返回。如果缓存没有命中的话,则写入Local Cache,最后返回结果给用户。

boundsql对象的详细信息:

CacheKey对象的CreateKey *** 作:

首先创建一个cachekey,默认hashcode=17,multiplIEr=37,count=0,updateList初始化。update *** 作:count++,对checksum,hashcode进行赋值,最后将参数添加到updateList中。
  //根据传入信息,创建chachekey  @OverrIDe  public CacheKey createCacheKey(MappedStatement ms,Boundsql boundsql) {    //执行器关闭就抛出异常    if (closed) {      throw new ExecutorException("Executor was closed.");    }    //创建一个cachekey,默认hashcode=17,multiplIEr=37,count=0,updateList初始化    CacheKey cacheKey = new CacheKey();    //添加 *** 作:sql的ID,逻辑分页偏移量,逻辑分页起始量,@R_301_5967@。    cacheKey.update(ms.getID());    cacheKey.update(rowBounds.getoffset());    cacheKey.update(rowBounds.getlimit());    cacheKey.update(boundsql.getsql());    List<ParameterMapPing> parameterMapPings = boundsql.getParameterMapPings();    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();    // mimic DefaultParameterHandler logic    for (ParameterMapPing parameterMapPing : parameterMapPings) {      if (parameterMapPing.getMode() != ParameterMode.OUT) {        Object value;        //参数名        String propertyname = parameterMapPing.getproperty();        //根据参数名获取值        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 = configuration.newMetaObject(parameterObject);          value = MetaObject.getValue(propertyname);        }        //添加参数值        cacheKey.update(value);      }    }    //添加environment的ID名,如果它不为空的话    if (configuration.getEnvironment() != null) {      // issue #176      cacheKey.update(configuration.getEnvironment().getID());    }    //返回cachekey    return cacheKey;  }

所以缓存项的key最后表示为:hashcode:checknum:遍历updateList,以:间隔

2020122321:657338105:com.smday.dao.IUserDao.findByID:0:2147483647:select * from user where ID = ?:41:MysqL

接着,调用同类中的query方法,针对是否开启二级缓存做不同的决断。(需要注意的是,这一部分是建立在cacheEnabled设置为true的前提下,当然默认是true。如果为false,Executor将会创建BaseExecutor,并不会判断mappers映射文件中二级缓存是否存在,而是直接执行delegate.<E> query(ms,boundsql)

  //主配置文件已经开启二级缓存  @OverrIDe  public <E> List<E> query(MappedStatement ms,ResultHandler resultHandler,CacheKey key,Boundsql boundsql)      throws sqlException {    Cache cache = ms.getCache();    //映射文件配置已经开启二级缓存    if (cache != null) {      //如果cache不为空,且需要清缓存的话(insert|update|delete),执行tcm.clear(cache);      flushCacheIfrequired(ms);      if (ms.isUseCache() && resultHandler == null) {        ensureNoOutParams(ms,boundsql);        @SuppressWarnings("unchecked")        //从缓存中获取        List<E> List = (List<E>) tcm.getobject(cache,key);        if (List == null) {          //缓存中没有就执行查询,BaseExecutor的query          List = delegate.<E> query(ms,boundsql);          //存入缓存          tcm.putObject(cache,List); // issue #578 and #116        }        //如果缓存中有,就直接返回        return List;      }    }    //映射文件没有开启二级缓存,需要进行查询,delegate其实还是Executor对象    return delegate.<E> query(ms,boundsql);  }

除了select *** 作之外,其他的的 *** 作都会清空二级缓存。XMLStatementBuilder中配置属性的时候:boolean flushCache = context.getBooleanAttribute("flushCache",!isSelect);

  private voID flushCacheIfrequired(MappedStatement ms) {    Cache cache = ms.getCache();    if (cache != null && ms.isFlushCacherequired()) {      //tcm后面会总结,清空二级缓存      tcm.clear(cache);    }  }
四、二级缓存TransactionCache

这里学习一下二级缓存涉及的缓存类:TransactionCache,同样也是基于装饰者设计模式,对传入的Cache进行装饰,构建二级缓存事务缓冲区:

CachingExecutor维护了一个TransactionCacheManager,即tcm,而这个tcm其实维护的就是一个key为Cache,value为TransactionCache包装过的Cache。而tcm.getobject(cache,key)的意思我们可以通过以下源码得知:

  public Object getobject(Cache cache,CacheKey key) {    //将传入的cache包装为TransactionalCache,并根据key获取值    return getTransactionalCache(cache).getobject(key);  }

需要注意的是,getobject方法中将会把获取值的职责一路向后传递,直到最基础的perpetualCache,根据cachekey获取。

最终获取到的值,如果为null,就需要把key加入未命中条目的缓存。

  @OverrIDe  public Object getobject(Object key) {    //根据职责一路向后传递    Object object = delegate.getobject(key);    if (object == null) {      //没找到值就将key存入未命中的set      entrIEsMissedInCache.add(key);    }    // issue #146    if (clearOnCommit) {      return null;    } else {      return object;    }  }

如果缓存中没有找到,将会从数据库中查找,查询到之后,将会进行添加 *** 作,也就是:tcm.putObject(cache,List);。我们可以发现,其实它并没有直接将数据加入缓存,而是将数据添加进待提交的map中。

  @OverrIDe  public voID putObject(Object key,Object object) {    entrIEsToAddOnCommit.put(key,object);  }

也就是说,一定需要某种手段才能让他真正地存入缓存,没错了,commit是可以的:

  //CachingExecutor.java  @OverrIDe  public voID commit(boolean required) throws sqlException {    //清除本地缓存    delegate.commit(required);    //调用tcm.commit    tcm.commit();  }

最终调用的是TransactionCache的commit方法:

  public voID commit() {    if (clearOnCommit) {      delegate.clear();    }    flushPendingEntrIEs();    reset();  }

最后的最后,我们可以看到将刚才的未命中和待提交的数据都进行了相应的处理,这才是最终影响二级缓存中数据的 *** 作,当然这中间也存在着职责链,就不赘述了。

当然,除了commit,close也是一样的,因为最终调用的其实都是commit方法,同样也会 *** 作缓存。

五、二级缓存测试
    <!-- 开启全局配置 -->    <settings>        <!--全局开启缓存配置,是默认开启的-->        <setting name="cacheEnabled" value="true"/>    </settings>
    <!-- 映射配置文件 -->    <!--开启user支持二级缓存-->    <cache></cache>    <select ID="findByID" resultType="user" useCache="true" >        select * from user where ID = #{ID}    </select>
    /**     * 测试二级缓存     */    @Test    public voID testFirstLevelCache2(){        sqlSession sqlSession1 = factory.openSession();        IUserDao userDao1 = sqlSession1.getMapper(IUserDao.class);        User user1 = userDao1.findByID(41);        System.out.printf("==> %s\n",user1);        sqlSession1.commit();        //sqlSession1.close();                sqlSession sqlSession2 = factory.openSession();        IUserDao userDao2 = sqlSession2.getMapper(IUserDao.class);        User user2 = userDao2.findByID(41);        System.out.printf("==> %s\n",user2);        sqlSession2.close();        System.out.println("user1 == user2:"+(user1 == user2));                sqlSession sqlSession3 = factory.openSession();        IUserDao userDao3 = sqlSession3.getMapper(IUserDao.class);        User user3 = userDao3.findByID(41);        System.out.printf("==> %s\n",user3);        sqlSession2.close();        System.out.println("user2 == user3:"+(user2 == user3));    }

二级缓存实现了sqlSession之间缓存数据的共享,是mapper映射级别的缓存。

有时缓存也会带来数据读取正确性的问题,如果数据更新频繁,会导致从缓存中读取到的数据并不是最新的,可以关闭二级缓存。

六、一级缓存源码解析

主配置文件或映射文件没有配置二级缓存开启,或者二级缓存中不存在数据,最终都会执行BaseExecutor的query方法,如果queryStack为空或者不是select语句,就会先清空本地的缓存。

    if (queryStack == 0 && ms.isFlushCacherequired()) {      clearLocalCache();    }

查看本地缓存(一级缓存)是否有数据,如果有直接返回,如果没有,则调用queryFromDatabase从数据库中查询。

List = resultHandler == null ? (List<E>) localCache.getobject(key) : null;if (List != null) {    //处理存储过程    handleLocallyCachedOutputParameters(ms,parameter,boundsql);} else {    //从数据库中查询    List = queryFromDatabase(ms,boundsql);}

判断本地缓存的级别是否为STATEMENT级别,如果是的话,清空缓存,因此STATEMENT级别的一级缓存无法共享localCache。

      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {        // issue #482        clearLocalCache();      }
七、测试一级缓存
    /**     * 测试一级缓存     */    @Test    public voID testFirstLevelCache1(){        sqlSession sqlSession1 = factory.openSession();        IUserDao userDao1 = sqlSession1.getMapper(IUserDao.class);        User user1 = userDao1.findByID(41);        System.out.printf("==> %s\n",user1);        IUserDao userDao2 = sqlSession1.getMapper(IUserDao.class);        User user2 = userDao2.findByID(41);        System.out.printf("==> %s\n",user2);        sqlSession1.close();        System.out.println("user1 == user2:"+(user1 == user2));    }

一级缓存默认是sqlSession级别地缓存,insert|delete|update|commit()和close()的 *** 作的执行都会清空一级缓存。

怎么说呢,分析源码的过程让我对Mybatis有了更加深刻的认识,可能有些理解还是没有很到位,或许是经验不足,很多东西还是浮于表面,但一翻deBUG下来,看到自己之前一个又一个的迷惑被非常确切地解开,真的爽!

https://www.jianshu.com/p/c553169c5921

总结

以上是内存溢出为你收集整理的mybatis源码学习:一级缓存和二级缓存分析全部内容,希望文章能够帮你解决mybatis源码学习:一级缓存和二级缓存分析所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/1218861.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-05
下一篇 2022-06-05

发表评论

登录后才能评论

评论列表(0条)

保存