Mybatis的缓存

作者: 三斤牛肉 | 来源:发表于2019-08-27 15:59 被阅读0次

    先看下Mybatis中的缓存类:


    image.png

    一级缓存

    mybatis默认自带了一级缓存,生命周期是一个sqlSession里

    我们来看下源代码中实现:

    @Override
      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 (closed) {
          throw new ExecutorException("Executor was closed.");
        }
        if (queryStack == 0 && ms.isFlushCacheRequired()) {
          clearLocalCache();
        }
        List<E> list;
        try {
          queryStack++;
    //从本地缓存中读取
          list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
          if (list != null) {//如果缓存中存在,则处理缓存数据
            handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
          } else {//否则从数据库读取
            list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
          }
        } finally {
          queryStack--;
        }
        if (queryStack == 0) {
          for (DeferredLoad deferredLoad : deferredLoads) {
            deferredLoad.load();
          }
          // issue #601
          deferredLoads.clear();
          if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            // issue #482
            clearLocalCache();
          }
        }
        return list;
      }
    
    //update的时候直接清空缓存,commit和rollback的时候同样,这样比较简单粗暴
    @Override
      public int update(MappedStatement ms, Object parameter) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
        if (closed) {
          throw new ExecutorException("Executor was closed.");
        }
        clearLocalCache();
        return doUpdate(ms, parameter);
      }
    
    //生成cachekey,将查询的一些关键属性作为key
    @Override
      public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
        if (closed) {
          throw new ExecutorException("Executor was closed.");
        }
        CacheKey cacheKey = new CacheKey();
        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);
          }
        }
        if (configuration.getEnvironment() != null) {
          // issue #176
          cacheKey.update(configuration.getEnvironment().getId());
        }
        return cacheKey;
      }
    

    全局变量中可以找到protected PerpetualCache localCache;
    展开PerpetualCache其实就是最基本的hashmap了。

    二级缓存

    默认二级缓存是关闭的

    Mybatis的二级缓存有2个开关:
    • Configuration中的cacheEnabled是全局开关,用来判断是否要创建CachingExecutor,默认是开启的。但是...
    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);
        }
    //当cacheEnabled=true时会用CachingExecutor代理原执行器
        if (cacheEnabled) {
          executor = new CachingExecutor(executor);
        }
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
      }
    
    • 在每个MappedStatement(可以理解为每一个Mapper.xml中sql语句的映射)中单独又有一个useCache的开关
    private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
        String resultType = nodeToHandle.getStringAttribute("resultType");
        Class<?> resultTypeClass = resolveClass(resultType);
        StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
        String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
        String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
        boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));
    
        //defaults
      // 默认是false
        boolean useCache = false;
        boolean resultOrdered = false;
        KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
        Integer fetchSize = null;
        Integer timeout = null;
        boolean flushCache = false;
        String parameterMap = null;
        String resultMap = null;
        ResultSetType resultSetTypeEnum = null;
    
        SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
        SqlCommandType sqlCommandType = SqlCommandType.SELECT;
    
    //
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
            fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
            resultSetTypeEnum, flushCache, useCache, resultOrdered,
            keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);
    
        id = builderAssistant.applyCurrentNamespace(id, false);
    
        MappedStatement keyStatement = configuration.getMappedStatement(id, false);
        configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
      }
    
    public MappedStatement addMappedStatement{
    ...
    //如果是select语句默认是要开启缓存的
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
            .resource(resource)
            .fetchSize(fetchSize)
            .timeout(timeout)
            .statementType(statementType)
            .keyGenerator(keyGenerator)
            .keyProperty(keyProperty)
            .keyColumn(keyColumn)
            .databaseId(databaseId)
            .lang(lang)
            .resultOrdered(resultOrdered)
            .resultSets(resultSets)
            .resultMaps(getStatementResultMaps(resultMap, resultType, id))
            .resultSetType(resultSetType)
            .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
    //默认是false
            .useCache(valueOrDefault(useCache, isSelect))
            .cache(currentCache);//currentCache是从哪儿来的呢
    ...
    }
    

    上面的代码可以看到.cache(currentCache),缓存用的是currentCache,那么这个currentCache是怎么来的呢,
    看下面的代码:
    可以看到cache是在解析Mapper.xml的时候配置的,每个cache以namespace作为key分隔:
    <mapper namespace="XXXXXXX.AccountInfoMapper">

    private void configurationElement(XNode context) {
        try {
          String namespace = context.getStringAttribute("namespace");
          if (namespace == null || namespace.equals("")) {
            throw new BuilderException("Mapper's namespace cannot be empty");
          }
          builderAssistant.setCurrentNamespace(namespace);
          cacheRefElement(context.evalNode("cache-ref"));
          cacheElement(context.evalNode("cache"));
          parameterMapElement(context.evalNodes("/mapper/parameterMap"));
          resultMapElements(context.evalNodes("/mapper/resultMap"));
          sqlElement(context.evalNodes("/mapper/sql"));
          buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
        } catch (Exception e) {
          throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
        }
      }
    
    public Cache useCacheRef(String namespace) {
        if (namespace == null) {
          throw new BuilderException("cache-ref element requires a namespace attribute.");
        }
        try {
          unresolvedCacheRef = true;
          Cache cache = configuration.getCache(namespace);
          if (cache == null) {
            throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
          }
          currentCache = cache;
          unresolvedCacheRef = false;
          return cache;
        } catch (IllegalArgumentException e) {
          throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
        }
      }
    

    那么这样的缓存策略有什么问题呢,一般来说我们通过MybatisGenerater生成的每一个Mapper都有一个独立的namespace,乍一看没啥问题。
    但是当自动生成不满足我们需求,需要手写的时候问题就出现了,
    假如有2个mapper:accountMapper、accountInfoMapper,后者在自己的namespace内修改了account,而前者是感知不到的,这个时候就会出现缓存不一致的问题,所以为了避免出现这种情况,尽量少用二级缓存。

    二级缓存的核心是CachingExecutor类

    CachingExecutor:

    @Override
      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) {
          flushCacheIfRequired(ms);
    //这里不深入展开,只要是select的useCache为true
          if (ms.isUseCache() && resultHandler == null) {
            ensureNoOutParams(ms, boundSql);
            @SuppressWarnings("unchecked")
            List<E> list = (List<E>) tcm.getObject(cache, key);
            if (list == null) {
              list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
              tcm.putObject(cache, key, list); // issue #578 and #116
            }
            return list;
          }
        }
        return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
      }
    

    再看tmc(TransactionalCacheManager) & TransactionalCache:

    /**
    *事务缓存,只有提交的时候才会放到缓存中,回滚的时候清空
    **/
    public class TransactionalCacheManager {
    
      private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
    
      public void clear(Cache cache) {
        getTransactionalCache(cache).clear();
      }
    
      public Object getObject(Cache cache, CacheKey key) {
        return getTransactionalCache(cache).getObject(key);
      }
      
      public void putObject(Cache cache, CacheKey key, Object value) {
        getTransactionalCache(cache).putObject(key, value);
      }
    
      public void commit() {
        for (TransactionalCache txCache : transactionalCaches.values()) {
          txCache.commit();
        }
      }
    
      public void rollback() {
        for (TransactionalCache txCache : transactionalCaches.values()) {
          txCache.rollback();
        }
      }
    
      private TransactionalCache getTransactionalCache(Cache cache) {
        TransactionalCache txCache = transactionalCaches.get(cache);
        if (txCache == null) {
          txCache = new TransactionalCache(cache);
          transactionalCaches.put(cache, txCache);
        }
        return txCache;
      }
    
    }
    
    /**
     * The 2nd level cache transactional buffer.
     * 
     * This class holds all cache entries that are to be added to the 2nd level cache during a Session.
     * Entries are sent to the cache when commit is called or discarded if the Session is rolled back. 
     * Blocking cache support has been added. Therefore any get() that returns a cache miss 
     * will be followed by a put() so any lock associated with the key can be released. 
     * 
     * @author Clinton Begin
     * @author Eduardo Macarron
     */
    public class TransactionalCache implements Cache {
    
      private static final Log log = LogFactory.getLog(TransactionalCache.class);
    
      private final Cache delegate;
      private boolean clearOnCommit;
      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) {
        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();
      }
    
      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);
          }
        }
      }
    
    }
    

    可以看到,二级缓存是在原缓存的基础上加了一层代理,代理类存放临时的结果集,当事务提交后,再将临时的结果集刷到MappedStatement关联的缓存中。可以理解为每个二级缓存是按sql模板做区分的

    相关文章

      网友评论

        本文标题:Mybatis的缓存

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