美文网首页
Mybatis缓存介绍

Mybatis缓存介绍

作者: 我不吃甜食 | 来源:发表于2018-10-11 18:36 被阅读0次

    Mybatis版本 3.4.6

    配置

    config配置

    <configuration>
        <properties resource="db.properties"></properties>
        <settings>
            <setting name="logImpl" value="STDOUT_LOGGING"/>
            <!--一级缓存默认打开,下面这一行关闭了一级缓存-->
            <!--<setting name="localCacheScope" value="STATEMENT"/>-->
            <!--二级缓存默认打开,除非配置了下面的setting-->
            <setting name="cacheEnabled" value="false"/>
        </settings>
        <environments default="development">
            <environment id="development">
                <transactionManager type="JDBC"/>
                <dataSource type="POOLED">
                    <property name="driver" value="${driver}"/>
                    <property name="url" value="${url}"/>
                    <property name="username" value="${username}"/>
                    <property name="password" value="${password}"/>
                </dataSource>
            </environment>
        </environments>
        <mappers>
            <mapper resource="personMapper.xml"/>
        </mappers>
    </configuration>
    

    一级二级缓存都是默认有效的,除非使用上述配置显式关闭。若想使用二级缓存,还必须在mapper.xml中配置<cache />,否则也无法使用二级缓存。

    一级缓存

    我们知道,Mybatis真正运行SQL的操作都委托给了BaseExecutor.java类,每次新open一个SqlSession,都会新建一个BaseExecutor,而一级缓存实际是BaseExecutor的一个实例变量,所以一级缓存是SqlSession独有的,不同的SqlSession不能共享;
    请看下面查询操作:

    //代码-1 BaseExecutor的query方法
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
          ......
          list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
          if (list != null) {
            handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
          } else {
            //若localCache中没有,则从数据库中查询并放入localCache中
            list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
          }
      
        if (queryStack == 0) {
          ......
          //若配置中配了<setting name="localCacheScope" value="STATEMENT"/>,则这句话就会生效,那么localCache就会被清空。
          //效果就是关闭了一级缓存,每次查询都会查到数据库中。
          if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            clearLocalCache(); 
          }
        }
        return list;
      }
    

    笔者觉得上述关闭一级缓存的方式并不优雅,因为总是要先把结果放入localCache中,如要关闭一级缓存,再清空。

    一级缓存分成Session 和 Statement两种,Statement实际上就是没有一级缓存。

    由于一级缓存是SqlSession级别的,这样就可能引发一个问题,SqlSession1修改了数据库,SqlSession2却感知不到这一变化,读到一级缓存中的脏数据。

    二级缓存

    二级缓存相关的类是CacheingExecutor.java,它的实例变量private final TransactionalCacheManager tcm = new TransactionalCacheManager()实际上就管理着二级缓存,其查询方法如下:

     public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
          throws SQLException {
        //这里的cache就是二级缓存
        Cache cache = ms.getCache(); 
        if (cache != null) {
          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查询;BaseExecutor中会查询一级缓存
              list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
              tcm.putObject(cache, key, list); // A
            }
            return list;
          }
        }
        return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
      }
    

    Mybatis中的二级缓存是通过TransactionalCacheManager(tcm)维护的, 真正的二级缓存是TransactionalCache

    public class TransactionalCache implements Cache {
      //存储我们查询到的数据的地方
      private final Cache delegate;
      private boolean clearOnCommit;
    
      //查询的数据会先暂时放在这里,等commit()后,才能移到上面的delegate里
      private final Map<Object, Object> entriesToAddOnCommit;
      private final Set<Object> entriesMissedInCache;
    
      public TransactionalCache(Cache delegate) {
        this.delegate = delegate;
        this.clearOnCommit = false;
        this.entriesToAddOnCommit = new HashMap<Object, Object>();
        this.entriesMissedInCache = new HashSet<Object>();
      }
    
      @Override
      public String getId() {
        return delegate.getId();
      }
    
      @Override
      public int getSize() {
        return delegate.getSize();
      }
    
      @Override
      public Object getObject(Object key) {
        // issue #116
        Object object = delegate.getObject(key);
        if (object == null) {
          entriesMissedInCache.add(key);
        }
        // issue #146
        if (clearOnCommit) {
          return null;
        } else {
          return object;
        }
      }
    
      @Override
      public ReadWriteLock getReadWriteLock() {
        return null;
      }
    
      @Override
      public void putObject(Object key, Object object) {
        //这里可以发现,代码A并没有把查询结果真正放入到缓存里,只是放在了entriesToAddOnCommit这个临时的地方
        entriesToAddOnCommit.put(key, object);
      }
    
      @Override
      public Object removeObject(Object key) {
        return null;
      }
    
      @Override
      public void clear() {
        clearOnCommit = true;
        entriesToAddOnCommit.clear();
      }
    
      //调用该方法,查询结果才会放入二级缓存
      public void commit() {
        if (clearOnCommit) {
          delegate.clear();
        }
        //把临时保存的数据保存到缓存中
        flushPendingEntries();
        reset();
      }
    
      public void rollback() {
        unlockMissedEntries();
        reset();
      }
    
      private void reset() {
        clearOnCommit = false;
        entriesToAddOnCommit.clear();
        entriesMissedInCache.clear();
      }
    
      //一次commit,可以将一个事务中多次查询的结果一次性放入缓存
      private void flushPendingEntries() {
        for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
          delegate.putObject(entry.getKey(), entry.getValue());
        }
        for (Object entry : entriesMissedInCache) {
          if (!entriesToAddOnCommit.containsKey(entry)) {
            delegate.putObject(entry, null);
          }
        }
      }
    
      private void unlockMissedEntries() {
        for (Object entry : entriesMissedInCache) {
          try {
            delegate.removeObject(entry);
          } catch (Exception e) {
            log.warn("Unexpected exception while notifiying a rollback to the cache adapter."
                + "Consider upgrading your cache adapter to the latest version.  Cause: " + e);
          }
        }
      }
    }
    
    缓存的初始化

    从上文可知,缓存是与Executor关联在一起,使用不同的Executor执行sql,就会使用不同的缓存。下面看一下Executor的初始化:
    Configuration.java

    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) { //可以通过setting配置为false;默认为true;
          executor = new CachingExecutor(executor); //二级缓存
        }
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
      }
    
    二级缓存的初始化

    二级缓存是通过解析mapper.xml中的<cache />或<cache-ref />进行初始化的,若没有这两个中的任意一个配置,是无法使用二级缓存的。
    XMLMapperBuilder.java

    private void cacheElement(XNode context) throws Exception {
        if (context != null) {
          String type = context.getStringAttribute("type", "PERPETUAL");
          Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
          String eviction = context.getStringAttribute("eviction", "LRU");
          Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
          Long flushInterval = context.getLongAttribute("flushInterval");
          Integer size = context.getIntAttribute("size");
          boolean readWrite = !context.getBooleanAttribute("readOnly", false);
          boolean blocking = context.getBooleanAttribute("blocking", false);
          Properties props = context.getChildrenAsProperties();
          builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
        }
      }
    

    上面的attribute实际上就是cache标签的属性,这里实例化了各种Cache,并进行了装饰,装饰后,默认情况下如下图所示;然后将该cache的引用赋值给MappedStatement实例,MappedStatement实例代表了一个sql,这样就把mapper.xml的sql语句与cache联系到了一起。当该sql查询出结果后,放入该cache中并交给TransactionalCacheManager管理。


    Cache.png

    本文参考了这里

    相关文章

      网友评论

          本文标题:Mybatis缓存介绍

          本文链接:https://www.haomeiwen.com/subject/ymfdwftx.html