zoukankan      html  css  js  c++  java
  • Mybatis 源码分析之一二级缓存

    一级缓存

    其实关于 Mybatis 的一级缓存是比较抽象的,并没有什么特别的配置,都是在代码中体现出来的。

    当调用 Configuration 的 newExecutor 方法来创建 executor:

    public Executor newExecutor(Transaction transaction, ExecutorType executorType, boolean autoCommit) {
        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) {
          executor = new CachingExecutor(executor, autoCommit);
        }
        // 执行对插件的调用
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
    }

    默认的 executorType 是 ExecutorType.SIMPLE(SimpleExecutor)。cacheEnabled 默认为 true ,所以一般情况下都会创建 CachingExecutor。

    当我们要使全局的映射器禁用缓存,可以配置 cacheEnabled 为false:

    在 CacheingExecutor 的 query 方法中:

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

    即使没有创建 CachingExecutor,在 BaseExecutor 的 query 方法中同样操作:

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

    不同的是,CachingExecutor 会在 MappedStatement 中获取 Cache,如果为 null,直接调用 BaseExecutor 的 query 方法:

    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();
          }
          deferredLoads.clear(); // issue #601
          if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            clearLocalCache(); // issue #482
          }
        }
        return list;
    }

    可以看到默认使用了 localCache,这个 localCache 是 PerpetualCache 类型的,基于 HashMap 实现。不管是使用哪种 Cache,CacheKey 都是通过 BaseExecutor 来创建:

    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();
        if (parameterMappings.size() > 0 && parameterObject != null) {
          TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
          if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            cacheKey.update(parameterObject);
          } else {
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            for (ParameterMapping parameterMapping : parameterMappings) {
              String propertyName = parameterMapping.getProperty();
              if (metaObject.hasGetter(propertyName)) {
                cacheKey.update(metaObject.getValue(propertyName));
              } else if (boundSql.hasAdditionalParameter(propertyName)) {
                cacheKey.update(boundSql.getAdditionalParameter(propertyName));
              }
            }
          }
        }
        return cacheKey;
    }

    这个 CacheKey 主要使用 hashCode 来构建唯一标识,默认的 hashCode 为 17,每一次 update 都会更新这个 hashCode :

      public void update(Object object) {
        int baseHashCode = object == null ? 1 : object.hashCode();
    
        count++;
        checksum += baseHashCode;
        baseHashCode *= count;
    
        hashcode = multiplier * hashcode + baseHashCode;
    
        updateList.add(object);
      }

    如果一个查询的 id、分页组件中的 offset 和 limit、sql 语句、参数 都保持不变,那么这个查询产生的 CacheKey一定是不变的。

    在一个 SqlSession 的生命周期内,二次同样的查询 CacheKey 是一样的:

    -1182036712:853128989:com.fcs.demo.dao.UserMapper.selectUserMaps:0:2147483647:select * from tb_user
    
    -1182036712:853128989:com.fcs.demo.dao.UserMapper.selectUserMaps:0:2147483647:select * from tb_user
    

    为什么强调是在一个 SqlSession 的生命周期内? PerpetualCache 类型的 localCache 被 Executor 持有,而特定类型的 Executor 又是被 DefaultSqlSession 持有,当 SqlSession 被关闭后,这些都不复存在。

    所以这个 localCache 就是 Mybatis 的一级缓存,不受任何配置影响,SqlSession 级别的。

    二级缓存

    一开始听说 MyBatis 的一二级缓存,我以为是两种完全沾不上边的东西,后来发现这二者竟然在同一个方法里碰过面,那就是 CachingExecutor 的 query 方法:

    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);
          if (ms.isUseCache() && resultHandler == null) { 
            ensureNoOutParams(ms, key, parameterObject, boundSql);
            if (!dirty) {
              cache.getReadWriteLock().readLock().lock();
              try {
                @SuppressWarnings("unchecked")
                List<E> cachedList = (List<E>) cache.getObject(key);
                if (cachedList != null) return cachedList;
              } finally {
                cache.getReadWriteLock().readLock().unlock();
              }
            }
            List<E> list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
            tcm.putObject(cache, key, list); // issue #578. Query must be not synchronized to prevent deadlocks
            return list;
          }
        }
        return delegate.<E>query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }

    首先 MappedStatement 中获取 cache,这个 cache 就是所谓的二级缓存,如果这个 cache 存在,将优先去这个 cache 中查找,如果找不到结果,那就走一级缓存的路子。

    突然觉得这个设计很棒啊,有点层层筛选的意思,这个筛网就是特定的 CacheKey,二级缓存筛出来了,就不需要再到一级缓存去筛了,如果一级也筛不出来,那就掉到最下面的容器里去了(数据库)。

    那么二级缓存是什么级别的?这个就要看 Cache 的来源了,上面显示是从 MappedStatement 中取出来的。而 MappedStatement 是通过 MapperBuilderAssistant 的 addMappedStatement 方法构建的:

    setStatementCache(isSelect, flushCache, useCache, currentCache, statementBuilder);

    这个方法有三个参数值得关注:flushCache、useCache、currentCache。

    而 currentCache 在下面这个方法中可以赋值(还有参照缓存相关的 useCacheRef 方法):

    public Cache useNewCache(Class<? extends Cache> typeClass,
          Class<? extends Cache> evictionClass,
          Long flushInterval,
          Integer size,
          boolean readWrite,
          Properties props) {
        typeClass = valueOrDefault(typeClass, PerpetualCache.class);
        evictionClass = valueOrDefault(evictionClass, LruCache.class);
        Cache cache = new CacheBuilder(currentNamespace)
            .implementation(typeClass)
            .addDecorator(evictionClass)
            .clearInterval(flushInterval)
            .size(size)
            .readWrite(readWrite)
            .properties(props)
            .build();
        configuration.addCache(cache);
        currentCache = cache;
        return cache;
    }

    useNewCache 方法是在解析 XML 文件的时候调用的:

    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);
          Properties props = context.getChildrenAsProperties();
          builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, props);
        }
    }

    可以看到如果仅仅配置一个:

    <cache/>

    将会采用默认的 type – PERPETUAL(PerpetualCache),默认的 eviction – LRU (最近最少使用的)算法。

    官方文档这样描述:

    • 映射语句文件中的所有 select 语句将会被缓存。
    • 映射语句文件中的所有 insert,update 和 delete 语句会刷新缓存。
    • 缓存会使用 Least Recently Used(LRU,最近最少使用的)算法来收回。
    • 根据时间表(比如 no Flush Interval,没有刷新间隔), 缓存不会以任何时间顺序来刷新。
    • 缓存会存储列表集合或对象(无论查询方法返回什么)的 1024 个引用。
    • 缓存会被视为是 read/write(可读/可写)的缓存,意味着对象检索不是共享的,而
      且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

    再回到开始的那个全局的映射器缓存是否启用的配置,如果 cacheEnabled 为 false,那个 CachingExecutor 就不会创建,即使你这里配置了 cache 也没有用。

    参照缓存

    某个时候,你会想在命名空间中共享相同的缓存配置和实例。在这样的情况下你可以使用 cache-ref 元素来引用另外一个缓存:

    <cache-ref namespace="com.someone.application.data.SomeMapper"/>

    在 useCacheRef 方法中是直接按命名空间去拿的:

    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);
        }
    }

    缓存失效与舍弃

    一级缓存失效

    再回顾下 Mybatis 和 Spring 结合使用时,mybatis-spring 所做的事:

    • MapperFactoryBean 通过继承 SqlSessionDaoSupport 获取了 sqlSession:
    public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
        if (!this.externalSqlSession) {
          this.sqlSession = new SqlSessionTemplate(sqlSessionFactory);
        }
    }
    • SqlSessionTemplate 通过代理来间接操纵 DefaultSqlSession:
    public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
          PersistenceExceptionTranslator exceptionTranslator) {
    
        notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
        notNull(executorType, "Property 'executorType' is required");
    
        this.sqlSessionFactory = sqlSessionFactory;
        this.executorType = executorType;
        this.exceptionTranslator = exceptionTranslator;
        this.sqlSessionProxy = (SqlSession) newProxyInstance(
            SqlSessionFactory.class.getClassLoader(),
            new Class[] { SqlSession.class },
            new SqlSessionInterceptor());
    }

    动态代理构建了方法的执行模板:

    private class SqlSessionInterceptor implements InvocationHandler {
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          final SqlSession sqlSession = getSqlSession(
              SqlSessionTemplate.this.sqlSessionFactory,
              SqlSessionTemplate.this.executorType,
              SqlSessionTemplate.this.exceptionTranslator);
          try {
            Object result = method.invoke(sqlSession, args);
            if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
              // force commit even on non-dirty sessions because some databases require
              // a commit/rollback before calling close()
              sqlSession.commit(true);
            }
            return result;
          } catch (Throwable t) {
            Throwable unwrapped = unwrapThrowable(t);
            if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
              Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
              if (translated != null) {
                unwrapped = translated;
              }
            }
            throw unwrapped;
          } finally {
            closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
          }
        }
    }
    • 通过 SqlSessionUtils 获取和关闭 SqlSession
    public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
    
        notNull(sessionFactory, "No SqlSessionFactory specified");
        notNull(executorType, "No ExecutorType specified");
    
        SqlSessionHolder holder = (SqlSessionHolder) getResource(sessionFactory);
    
        if (holder != null && holder.isSynchronizedWithTransaction()) {
          if (holder.getExecutorType() != executorType) {
            throw new TransientDataAccessResourceException("Cannot change the ExecutorType when there is an existing transaction");
          }
    
          holder.requested();
    
          if (logger.isDebugEnabled()) {
            logger.debug("Fetched SqlSession [" + holder.getSqlSession() + "] from current transaction");
          }
    
          return holder.getSqlSession();
        }
    
        if (logger.isDebugEnabled()) {
          logger.debug("Creating a new SqlSession");
        }
    
        SqlSession session = sessionFactory.openSession(executorType);
    
        //......
    
        return session;
    
    }

    获取和关闭并不是直接操作 SqlSession,这里有 SqlSessionHolder,通过 TransactionSynchronizationManager 的 getResource 方法来获取 SqlSessionHolder,如果 holder 不为 null 并且被当前事物锁定,则在 holder 中获取 SqlSession。

    public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) {
    
        notNull(session, "No SqlSession specified");
        notNull(sessionFactory, "No SqlSessionFactory specified");
    
        SqlSessionHolder holder = (SqlSessionHolder) getResource(sessionFactory);
        if ((holder != null) && (holder.getSqlSession() == session)) {
          if (logger.isDebugEnabled()) {
            logger.debug("Releasing transactional SqlSession [" + session + "]");
          }
          holder.released();
        } else {
          if (logger.isDebugEnabled()) {
            logger.debug("Closing non transactional SqlSession [" + session + "]");
          }
          session.close();
        }
    }

    SqlSession 如果重新获取,必然导致一级缓存失效。如果我们自己打开并关闭 SqlSession,这一切是可控的,但是和 Spring 一起使用时,就要注意这个问题。

    二级缓存舍弃

    看到了二级缓存,我不由自主找了一下,在我们的项目中并没有这个二级缓存的配置,这是为什么?既然可以避免重复查询,为啥不用呢?

    原来不同命名空间下的表存在关联查询的话,其中一个针对某个表做了修改,另外一个命名空间下的查询没有任何变化,还是关联的这个表,那么使用了缓存明显存在脏数据。

    所以如果表关联比较复杂的话,一般是不会使用二级缓存的。

  • 相关阅读:
    ASP.NET连接数据库配置文件
    ASP.NET应用程序的文件类型及文件夹列表
    c#配置文件的简单操作
    js加载XML文件
    c#生成动态库并加载
    class和id的区别
    Div和Span的区别
    C#类和对象
    C#表达式和语句
    函数声明提升和变量提升
  • 原文地址:https://www.cnblogs.com/lucare/p/9312611.html
Copyright © 2011-2022 走看看