Mybatis 缓存机制

Mybatis 的一级缓存与二级缓存

当我们使用 Mybatis 开启一次与数据库的会话,Mybatis 会创建出一个 SqlSession 对象表示一次与数据库会话。

在对数据库的一次会话中,我们有可能反复地执行相同的查询语句,如果不采取一些措施的话,每一次查询都会物理查询一次数据库,而我们在极短的时间内做了完全相同的查询,那么它们的结果极有可能完全相同,由于查询一次数据库的代价很大,这有可能造成很大的资源浪费。

为了解决这一问题,减少资源的浪费,Mybatis 会在表示会话的 SqlSession 对象中建立一个简单的缓存(确切地说,应该是 SqlSession 有两个非常重要的属性,即:configuration, executorconfiguration 代表 Mybatis 的配置信息,executor 代表执行语句的执行器,缓存就维护在 executor 对象中。),将每次查询到的结果缓存起来,当下次查询的时候,如果判断先前有个完全一样的查询,会直接从缓存中直接将结果取出,返回给用户,不需要再进行一次数据库查询了。

如图下图所示,Mybatis 的缓存用例:一个 SqlSession 对象中创建一个本地缓存(local cache),对于每一次查询,都会尝试根据查询条件去本地缓存中查找是否在缓存中,如果在缓存中,就直接从缓存取出,然后返回给前台,否则从数据库读取数据,将查询结果存入缓存并返回给前台。

mybatis缓存用例

1. 一级缓存

Mybatis 的一级缓存是针对 SqlSession 的,实际上,SqlSession 只是 Mybatis 的一个接口,SqlSession 将它的工作交给 Executor 执行器这个角色来完成,负责完成对数据库的各种操作。当创建一个 SqlSession 对象时,Mybatis 会为这个 SqlSession 对象创建一个新的 Executor 执行器,而缓存信息就被维护在这个 Executor 执行器中,Mybatis 将缓存和对缓存相关的操作封装了 Cache 接口中。SqlSession, Executor, Cache 之间的关系如下列类图所示:

sqlsession和executor和cache类图

如上面的类图所示,Executor 接口的实现类 BaseExecutor 中拥有一个 Cache 接口的实现类 PerpetualCache,则对于 BaseExecutor 对象而言,它将使用 PerpetualCache 对象维护缓存。

那么接下来看一下 PerpetualCache 的具体实现:

public class PerpetualCache implements Cache {

    private final String id;

    private Map<Object, Object> cache = new HashMap<>();

    public PerpetualCache(String id) {
        this.id = id;
    }

    @Override
    public String getId() {
        return id;
    }

    @Override
    public int getSize() {
        return cache.size();
    }

    @Override
    public void putObject(Object key, Object value) {
        cache.put(key, value);
    }

    @Override
    public Object getObject(Object key) {
        return cache.get(key);
    }

    @Override
    public Object removeObject(Object key) {
        return cache.remove(key);
    }

    @Override
    public void clear() {
        cache.clear();
    }

    @Override
    public boolean equals(Object o) {
        if (getId() == null) {
            throw new CacheException("Cache instances require an ID.");
        }
        if (this == o) {
            return true;
        }
        if (!(o instanceof Cache)) {
            return false;
        }

        Cache otherCache = (Cache) o;
        return getId().equals(otherCache.getId());
    }

    @Override
    public int hashCode() {
        if (getId() == null) {
            throw new CacheException("Cache instances require an ID.");
        }
        return getId().hashCode();
    }

}

上面的具体实现我们可以清楚的看到是由一个 HashMap 维护的。

1.1 一级缓存的生命周期

  1. Mybatis 在开启一个数据库会话时,会创建一个新的 SqlSession 对象,SqlSession 对象中会有一个新的 Executor 对象,Executor 对象中持有一个新的 PerpetualCache 对象。当会话结束时,SqlSession 对象及其内部的 Executor 对象和其内部的 PerpetualCache 对象一并释放掉。
  2. 如果 SqlSession 调用了 close 方法,会释放掉一级缓存 PerpetualCache 对象,一级缓存将不可用。
  3. 如果 SqlSession 调用了 clearCache 方法,会清空 PerpetualCache 对象中的数据,但是该对象仍然可以使用。
  4. SqlSession 中执行了任何一个 update 操作(insert, update, delete),都会清空 PerpetualCache 对象的数据,但是该对象可以继续使用。

mybatis一级缓存周期

1.2 一级缓存的工作流程

  1. 对于某个查询,根据 statementId, params, rowBounds 来构建一个 key 值,根据这个 key 值缓存 Cache 中取出对应的 key 值存储的缓存结果。
  2. 判断从 Cache 中根据特定的 key 值取的数据是否为空,即是否命中。
  3. 如果命中,则直接将缓存结果返回。
  4. 如果没命中:
    4.1. 去数据库中查询数据,得到查询结果
    4.2. 将 key 和查询到的结果分别作为 key, value 对存储到 Cache 中。
    4.3. 将查询结果返回。

mybatis查询工作时序图

1.3 Cache 接口的设计以及 CacheKey 的定义

Mybatis 定义了一个 org.apache.ibatis.cache.Cache 接口作为其 Cache 提供者的 SPI(Service Provider Integerface),所有的 Mybatis 内部的 Cache 缓存,都应该实现这一接口。Mybatis 定义了一个 PerpetualCache 实现类实现了 Cache 接口,Mybatis 内部还有很多 Cache 接口实现,一级缓存只会涉及到一个 PerpetualCache 实现类,其它实现类在二级缓存中介绍。

我们知道,Cache 最核心的实现其实就是一个 Map,将本次查询使用的特征值作为 key,将查询结果作为 value 存储到 Map 中。

现在最核心的问题出现了:怎样来确定一次查询的特征值?

换句话说就是:怎样判断某两次查询是完全相同的查询?

也可以这样说:如何确定 Cache 中的 key 值?

Mybatis 认为,对于两次查询,如果以下条件都完全一样,那么就认为它们是完全相同的两次查询:

  1. 传入的 statementId(对应着 XML 中定义的语句 id)。

  2. 查询时要求的结果集中的结果范围(结果范围通过 rowBounds.offsetrowBounds.limit 表示)。

    Mybatis 自身提供的分页功能是通过 RowBounds 来实现,它通过 rowBounds.offsetrowBounds.limit 来过滤查询出来的结果集,这种分页功能是基于查询结果的再过滤,而不是进行数据库的物理分页。

    由于 Mybatis 底层还是依赖于 JDBC 实现的,那么,对于两次完全一模一样的查询,Mybatis 要保证对于底层 JDBC 而言,也是完全一致的查询才行。而对于 JDBC 而言,两次查询,只要传入给 JDBC 的 SQL 语句完全一致,传入的参数也完全一致,就认为是两次查询是完全一致的。

  3. 这次查询所产生的最终要传递给 JDBC java.sql.PreparedStatementSQL 语句字符串(boundSql.getSql())。

  4. 传递给 java.sql.Statement 要设置的参数值。

综上所述,CacheKey 由以下条件决定:

  1. statementId
  2. rowBounds
  3. 传递给 JDBC 的 SQL
  4. 传递给 JDBC 的参数值
1.3.1 CacheKey 的创建

对于每次的查询请求,Executor 都会根据传递的参数信息以及动态生成的 SQL 语句,将上面的条件根据一定的计算规则,创建一个对应的 CacheKey 对象。

我们知道创建 CacheKey 的目的,就两个:

  1. 根据 CacheKey 作为 key, 去 Cache 缓存中查找缓存结果。
  2. 如果查找缓存命中失败,则通过此 CacheKey 作为 key,将从数据库查询到的结果作为 value,组成 key/value 对存储到 Cache 缓存中。

CacheKey 的构建被放置到了 Executor 接口的实现类 BaseExecutor 中:

// 延迟加载队列
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;

// BaseExecutor 类中的 createCacheKey 方法
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
        throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    // 1.statementId
    cacheKey.update(ms.getId());
    // 2.rowBounds.offset
    cacheKey.update(rowBounds.getOffset());
    // 3.rowBounds.limit
    cacheKey.update(rowBounds.getLimit());
    // 4.sql 语句
    cacheKey.update(boundSql.getSql());
    // 5.将每一个要传递给 JDBC 的参数值也更新到 CacheKey 中
    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);
            }
            // 将每一个要传递给JDBC的参数值也更新到 CacheKey 中  
            cacheKey.update(value);
        }
    }
    if (configuration.getEnvironment() != null) {
        // issue #176
        cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
}

// 从数据库查询数据的方法,具体存缓存的步骤也在方法内部
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    // EXECUTION_PLACEHOLDER 占位符,是一个枚举类型的常量
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
        // 做了查询拿到了结果集
        list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
        // 将占位符的 key 移除
        localCache.removeObject(key);
    }
    // 将查询的结果 按照 key 存放相应的 结果集
    localCache.putObject(key, list);
    // StatementType 有三个常量,STATEMENT, PREPARED, CALLABLE,代表 XML 中 sql 语句指定的属性 StatementType
    // 具体会使用哪种表现形式去构建 sql,如使用 $符号提前解析要拼接到 sql 中的内容,还是使用 #号代表 ? 预编译占位符,再或者是调用存储过程
    // 分别代表 ${}, #{}, 数据库中的存储过程
    if (ms.getStatementType() == StatementType.CALLABLE) {
        localOutputParameterCache.putObject(key, parameter);
    }
    return list;
}

// 下面是公有方法,供外界调用的查询方法
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    // 这里涉及到了 CacheKey 的创建,可以回顾
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    // 具体调用下面的 query 方法
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
// 在上面的查询会使用的 query 方法
@SuppressWarnings("unchecked")
@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.");
    }
    // 如果查询栈为 0 并且 XML sql 语句的标签(select,update,insert,delete)上设置 flushCache 为 true
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
        // 清除缓存
        clearLocalCache();
    }
    List<E> list;
    try {
        // 查询栈自增,网上有人说这块是为了在递归调用时,避免上面清除缓存
        queryStack++;
        // 如果 resultHandler 为空,则从缓存中获取结果集
        list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
        if (list != null) {
            // 如果从缓存中拿到了结果集,处理 localOutputParameterCache
            handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
        } else {
            // 如果结果集为空,则从数据库中查询结果集,并添加缓存
            list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
        }
    } finally {
        // 查询结束或者异常,查询栈自减
        queryStack--;
    }
    // 如果此时查询栈为 0 
    if (queryStack == 0) {
       	// 延迟加载队列中所有元素,队列指的是该类的一个 deferredLoads 属性,在本文中该类顶部可见
        for (DeferredLoad deferredLoad : deferredLoads) {
            // 跟到底层代码,是 Result 提取器从缓存中获取结果集,并赋值给对象的操作
            deferredLoad.load();
        }
        // issue #601
        deferredLoads.clear();
        // 如果配置的 一级缓存作用域 为 STATEMENT
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            // issue #482
            // 则 清空一级缓存
            clearLocalCache();
        }
    }
    return list;
}

// 清理一级缓存
@Override
public void clearLocalCache() {
    if (!closed) {
        localCache.clear();
        localOutputParameterCache.clear();
    }
}

// 处理 localOutputParameterCache
private void handleLocallyCachedOutputParameters(MappedStatement ms, CacheKey key, Object parameter, BoundSql boundSql) {
    if (ms.getStatementType() == StatementType.CALLABLE) {
        final Object cachedParameter = localOutputParameterCache.getObject(key);
        if (cachedParameter != null && parameter != null) {
            final MetaObject metaCachedParameter = configuration.newMetaObject(cachedParameter);
            final MetaObject metaParameter = configuration.newMetaObject(parameter);
            for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
                if (parameterMapping.getMode() != ParameterMode.IN) {
                    final String parameterName = parameterMapping.getProperty();
                    final Object cachedValue = metaCachedParameter.getValue(parameterName);
                    metaParameter.setValue(parameterName, cachedValue);
                }
            }
        }
    }
}

PerpetualCache 代码具体实现中,不难发现 Cache 实际上是由一个 HashMap 维护的,CacheKey 创建好了之后,肯定是要作为 Mapkey 存储起来的,那既然是作为 Mapkey 就不得不关注 CacheKeyhashcode 方法。

1.3.2 CacheKey 的 hashcode 生成算法

CacheKey 的源码如下:

public class CacheKey implements Cloneable, Serializable {

    private static final long serialVersionUID = 1146682552656046210L;

    public static final CacheKey NULL_CACHE_KEY = new NullCacheKey();

    private static final int DEFAULT_MULTIPLYER = 37;
    private static final int DEFAULT_HASHCODE = 17;

    private final int multiplier;
    private int hashcode;
    private long checksum;
    private int count;
    // 8/21/2017 - Sonarlint flags this as needing to be marked transient.  While true if content is not serializable, this is not always true and thus should not be marked transient.
    private List<Object> updateList;

    public CacheKey() {
        this.hashcode = DEFAULT_HASHCODE;
        this.multiplier = DEFAULT_MULTIPLYER;
        this.count = 0;
        this.updateList = new ArrayList<>();
    }

    public CacheKey(Object[] objects) {
        this();
        updateAll(objects);
    }

    public int getUpdateCount() {
        return updateList.size();
    }

    // 这个 update 方法是 rehash 的算法具体实现
    public void update(Object object) {
        // 如果 object 不是空,则以对象本身 hashCode 码作为基础的 hashCode
        int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);

        count++;
        checksum += baseHashCode;
        baseHashCode *= count;
        // hashcode 值会作为 CacheKey 的对象的 hashCode,可以查看一下 下面的 hashCode 方法和 equals 方法
        hashcode = multiplier * hashcode + baseHashCode;

        updateList.add(object);
    }

    public void updateAll(Object[] objects) {
        for (Object o : objects) {
            update(o);
        }
    }

    @Override
    public boolean equals(Object object) {
        if (this == object) {
            return true;
        }
        if (!(object instanceof CacheKey)) {
            return false;
        }

        final CacheKey cacheKey = (CacheKey) object;

        if (hashcode != cacheKey.hashcode) {
            return false;
        }
        if (checksum != cacheKey.checksum) {
            return false;
        }
        if (count != cacheKey.count) {
            return false;
        }

        for (int i = 0; i < updateList.size(); i++) {
            Object thisObject = updateList.get(i);
            Object thatObject = cacheKey.updateList.get(i);
            if (!ArrayUtil.equals(thisObject, thatObject)) {
                return false;
            }
        }
        return true;
    }

    @Override
    public int hashCode() {
        return hashcode;
    }

    @Override
    public String toString() {
        StringJoiner returnValue = new StringJoiner(":");
        returnValue.add(String.valueOf(hashcode));
        returnValue.add(String.valueOf(checksum));
        updateList.stream().map(ArrayUtil::toString).forEach(returnValue::add);
        return returnValue.toString();
    }

    @Override
    public CacheKey clone() throws CloneNotSupportedException {
        CacheKey clonedCacheKey = (CacheKey) super.clone();
        clonedCacheKey.updateList = new ArrayList<>(updateList);
        return clonedCacheKey;
    }

}

上面在调用 ArrayUtil.hashCode 方法时,可能想知道了解具体是怎么生成的:

 // 该方法即是 ArrayUtil 下的 hashCode 方法 

  /**
   * Returns a hash code for {@code obj}.
   *
   * @param obj
   *          The object to get a hash code for. May be an array or <code>null</code>.
   * @return A hash code of {@code obj} or 0 if {@code obj} is <code>null</code>
   */
  public static int hashCode(Object obj) {
    if (obj == null) {
      // for consistency with Arrays#hashCode() and Objects#hashCode()
      return 0;
    }
    final Class<?> clazz = obj.getClass();
    // 由上面我们看到的 createCacheKey 那个方法的源码,不难发现,都是 非数组的数据
    // 所以每次返回的都是对象本身的 hashCode  
    if (!clazz.isArray()) {
      return obj.hashCode();
    }
    final Class<?> componentType = clazz.getComponentType();
    if (long.class.equals(componentType)) {
      return Arrays.hashCode((long[]) obj);
    } else if (int.class.equals(componentType)) {
      return Arrays.hashCode((int[]) obj);
    } else if (short.class.equals(componentType)) {
      return Arrays.hashCode((short[]) obj);
    } else if (char.class.equals(componentType)) {
      return Arrays.hashCode((char[]) obj);
    } else if (byte.class.equals(componentType)) {
      return Arrays.hashCode((byte[]) obj);
    } else if (boolean.class.equals(componentType)) {
      return Arrays.hashCode((boolean[]) obj);
    } else if (float.class.equals(componentType)) {
      return Arrays.hashCode((float[]) obj);
    } else if (double.class.equals(componentType)) {
      return Arrays.hashCode((double[]) obj);
    } else {
      return Arrays.hashCode((Object[]) obj);
    }
  }

2. 二级缓存

Mybatis 的二级缓存是基于 namespace 的,它可以提高数据库查询的效率,以提高应用的性能。

2.1 Mybatis 的缓存机制整体设计以及二级缓存的工作模式

Mybatis的缓存机制整体设计以及二级缓存的工作模式

如上图所示,当开启一个会话时,一个 SqlSession 对象会使用 Executor 来完成会话操作。Mybatis 的二级缓存机制的关键就是这个 Executor 实例对象。Mybatis 二级缓存的总开关是开启的,为什么这么说,讲证据:

// 该方法为 SqlSessionFactoryBuilder 的方法
// 回到最初的起点,构建 SqlSessionFactory 的时候调用的方法
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
        XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
        // 在这里调用了 parse 方法来拿到 Configuration 对象
        return build(parser.parse());
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
        ErrorContext.instance().reset();
        try {
            reader.close();
        } catch (IOException e) {
            // Intentionally ignore. Prefer previous error.
        }
    }
}

下面来看一下构建 Configuration 对象都做了什么事情

// 下面的方法为 XMLConfigBuilder 的方法
public Configuration parse() {
    if (parsed) {
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}

// 实际解析 Configuration 标签的方法
private void parseConfiguration(XNode root) {
    try {
        //issue #117 read properties first
        propertiesElement(root.evalNode("properties"));
        Properties settings = settingsAsProperties(root.evalNode("settings"));
        loadCustomVfs(settings);
        loadCustomLogImpl(settings);
        typeAliasesElement(root.evalNode("typeAliases"));
        pluginElement(root.evalNode("plugins"));
        objectFactoryElement(root.evalNode("objectFactory"));
        objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        reflectorFactoryElement(root.evalNode("reflectorFactory"));
        // 注意这里,设置了配置信息以及默认值
        settingsElement(settings);
        // read it after objectFactory and objectWrapperFactory issue #631
        environmentsElement(root.evalNode("environments"));
        databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        typeHandlerElement(root.evalNode("typeHandlers"));
        mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}

// 设置配置信息以及默认值的方法
private void settingsElement(Properties props) {
    configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL")));
    configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE")));
    // 注意这里,mybatis 会根据 XML 中的配置项来读取是否支持缓存,缺省值为 true,即支持使用缓存
    // 也就是说 cacheEnabled 是二级缓存的总开关,如果这里为 false,则后续在执行sql的标签中即便使用 useCahe 属性也不会开启二级缓存
    configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
    configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty("proxyFactory")));
    configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty("lazyLoadingEnabled"), false));
    configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty("aggressiveLazyLoading"), false));
    configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty("multipleResultSetsEnabled"), true));
    configuration.setUseColumnLabel(booleanValueOf(props.getProperty("useColumnLabel"), true));
    configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty("useGeneratedKeys"), false));
    configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
    configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty("defaultStatementTimeout"), null));
    configuration.setDefaultFetchSize(integerValueOf(props.getProperty("defaultFetchSize"), null));
    configuration.setDefaultResultSetType(resolveResultSetType(props.getProperty("defaultResultSetType")));
    configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty("mapUnderscoreToCamelCase"), false));
    configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty("safeRowBoundsEnabled"), false));
    configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
    configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty("jdbcTypeForNull", "OTHER")));
    configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty("lazyLoadTriggerMethods"), "equals,clone,hashCode,toString"));
    configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty("safeResultHandlerEnabled"), true));
    configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty("defaultScriptingLanguage")));
    configuration.setDefaultEnumTypeHandler(resolveClass(props.getProperty("defaultEnumTypeHandler")));
    configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty("callSettersOnNulls"), false));
    configuration.setUseActualParamName(booleanValueOf(props.getProperty("useActualParamName"), true));
    configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty("returnInstanceForEmptyRow"), false));
    configuration.setLogPrefix(props.getProperty("logPrefix"));
    configuration.setConfigurationFactory(resolveClass(props.getProperty("configurationFactory")));
}

在上一篇文章 Mybatis 实现原理 中,我们知道流程的第一步是通过配置对象构建 SqlSessionFactory 对象,第二步是通过 SqlSessionFactory 对象来开启一个 SqlSession。(有兴趣的可以按住 Ctrl 键,左键单击 Mybatis 实现原理 来看一下上边提到的这两个步骤)

文章中提到了两个比较重要的方法:

  1. openSessionFromDataSource 通过数据源开启会话
  2. openSessionFromConnection 通过连接开启会话

下面我们具体看一下这两个方法(下面为 DefaultSqlSessionFactory 的部分源码):

// 这两个方法在 DefaultSqlSessionFactory 类中

/**
 * 通过数据源开启会话
 */
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
        final Environment environment = configuration.getEnvironment();
        final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        // 这里注意:在每次开启会话的时候,configuration 对象都会新创建一个 Executor 对象
        final Executor executor = configuration.newExecutor(tx, execType);
        return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
        closeTransaction(tx); // may have fetched a connection so lets call close()
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}
/**
 * 通过连接开启会话
 */
private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
    try {
        boolean autoCommit;
        try {
            autoCommit = connection.getAutoCommit();
        } catch (SQLException e) {
            // Failover to true, as most poor drivers
            // or databases won't support transactions
            autoCommit = true;
        }
        final Environment environment = configuration.getEnvironment();
        final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
        final Transaction tx = transactionFactory.newTransaction(connection);
        // 同理,这里也是会创建一个新的 Executor
        final Executor executor = configuration.newExecutor(tx, execType);
        return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

上面的源码中,我标注了会创建 Executor 实例的方法,接下来看一下这一部分代码(下面为 Configuration 部分源码):

// 这里为创建 Executor 的具体实现
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    // Executor 实例会根据 ExecutorType 来实例化
    // 默认是 SIMPLE
    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) {
        // 这里如果设置了 cacheEnabled=true 则使用 CachingExecutor 进行构建
        // 如果没有在 XML 中配置 cacheEnabled,默认是开启的
        // CachingExecutor 使用了装饰者设计模式
        executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

下面看一下如何使用的装饰者(下面代码为 CachingExecutor):

public class CachingExecutor implements Executor {
	
    private final Executor delegate;
    private final TransactionalCacheManager tcm = new TransactionalCacheManager();

    public CachingExecutor(Executor delegate) {
        // 初始化委托者
        this.delegate = delegate;
        // 给委托者设置包装类,这个方法最终会调用 BaseExecutor 的 setExecutorWrapper 方法
        // 也就是说会在这里将 CachingExecutor 实例赋值给 BaseExecutor 中的 protected Executor wrapper 属性
        // BaseExecutor 的子类有三种,SIMPLE,REUSE,BATCH,无论哪种,在查询的时候都会使用 wrapper 作为其中一个参数去构建 StatementHandler
        // 再根据具体 StatementHandler 执行指定类型的 Statement
        delegate.setExecutorWrapper(this);
    }

    @Override
    public Transaction getTransaction() {
        return delegate.getTransaction();
    }

    @Override
    public void close(boolean forceRollback) {
        try {
            //issues #499, #524 and #573
            if (forceRollback) {
                tcm.rollback();
            } else {
                tcm.commit();
            }
        } finally {
            delegate.close(forceRollback);
        }
    }

    @Override
    public boolean isClosed() {
        return delegate.isClosed();
    }

    @Override
    public int update(MappedStatement ms, Object parameterObject) throws SQLException {
        // 更新操作,如果执行 sql 的标签上配置了 flushCache=true 则会刷新,默认为 false
        flushCacheIfRequired(ms);
        return delegate.update(ms, parameterObject);
    }

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

    @Override
    public <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException {
        flushCacheIfRequired(ms);
        return delegate.queryCursor(ms, parameter, rowBounds);
    }
	/**
	 * 看一下查询方法
	 */
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
        throws SQLException {
        // 首先从 MappedStatement 中获取缓存实例,即在 mapper XML 中配置的 Cache 标签,Cache 的默认回收策略是 LRU
        // 如果没有配置 Cache 标签的话,则这里的 Cache 对象为空!
        Cache cache = ms.getCache();
        if (cache != null) {
            // 如果执行 sql 的标签上添加了 flushCache=true,默认如果没配置的话是 false
            // 内部执行 事务缓存管理器清除缓存项
            flushCacheIfRequired(ms);
            if (ms.isUseCache() && resultHandler == null) {
                // 如果执行 sql 的标签上配置了 useCache=true 并且 resultHandler 为空
                // 如果执行的是存储过程,要确保没有 out 参数,只有 in 参数
                ensureNoOutParams(ms, boundSql);
                // 从事务缓存中获取结果集
                @SuppressWarnings("unchecked")
                List<E> list = (List<E>) tcm.getObject(cache, key);
                if (list == null) {
                    // 如果缓存没有命中,再由本身类型的执行器去查询,并将结果集放入缓存
                    list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    tcm.putObject(cache, key, list); // issue #578 and #116
                }
                return list;
            }
        }
        // 如果缓存实例为空,或者没指定 useCache(select 类型的默认 useCache 为 true),
        // 再或者 resultHandler 不为空,则直接使用原类型的执行器去查询数据
        return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }

    @Override
    public List<BatchResult> flushStatements() throws SQLException {
        return delegate.flushStatements();
    }

    @Override
    public void commit(boolean required) throws SQLException {
        delegate.commit(required);
        tcm.commit();
    }

    @Override
    public void rollback(boolean required) throws SQLException {
        try {
            delegate.rollback(required);
        } finally {
            if (required) {
                tcm.rollback();
            }
        }
    }

    // 如果执行的是存储过程,要确保没有 out 参数,只有 in 参数
    private void ensureNoOutParams(MappedStatement ms, BoundSql boundSql) {
        if (ms.getStatementType() == StatementType.CALLABLE) {
            for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
                if (parameterMapping.getMode() != ParameterMode.IN) {
                    throw new ExecutorException("Caching stored procedures with OUT params is not supported.  Please configure useCache=false in " + ms.getId() + " statement.");
                }
            }
        }
    }

    @Override
    public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
        return delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql);
    }

    @Override
    public boolean isCached(MappedStatement ms, CacheKey key) {
        return delegate.isCached(ms, key);
    }

    @Override
    public void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType) {
        delegate.deferLoad(ms, resultObject, property, key, targetType);
    }

    @Override
    public void clearLocalCache() {
        delegate.clearLocalCache();
    }

    private void flushCacheIfRequired(MappedStatement ms) {
        Cache cache = ms.getCache();
        if (cache != null && ms.isFlushCacheRequired()) {
            tcm.clear(cache);
        }
    }

    @Override
    public void setExecutorWrapper(Executor executor) {
        throw new UnsupportedOperationException("This method should not be called");
    }

}

看完了上面装饰者的代码,我们来看一下如何开启,如何关闭二级缓存:

  1. 首先在 Mybatis 的配置 XML 中是否在 <settings></settings> 标签中的 <setting name="cacheEnabled" value="true"/> 如果没配置 cacheEnabled,缺省值为 true,也就是二级缓存的总开关是开启的,可以手动置为 false 关闭。

  2. Mybatis 二级缓存是基于 namespace 的即 Mapper,如果想要使用二级缓存,首先要在想要开启二级缓存的 Mapper 的 XML 中指定 <cache eviction="LRU" flushInterval="600000" size="4096" readOnly="true"/> 标签(只是一个配置样例,使用上要基于使用场景),如果未指定 <cache/> 标签,则不开启二级缓存,对应上面代码 cache 为空的状态。

  3. <select></select> 标签默认是开启二级缓存的:

    // XMLMapperBuilder 部分代码
    public void parse() {
        if (!configuration.isResourceLoaded(resource)) {
            // 如果没加载过就解析以下 Mapper XML 中 <mapper/> 标签
            configurationElement(parser.evalNode("/mapper"));
            configuration.addLoadedResource(resource);
            bindMapperForNamespace();
        }
    
        parsePendingResultMaps();
        parsePendingCacheRefs();
        parsePendingStatements();
    }
    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"));
            // 解析 cache 标签
            cacheElement(context.evalNode("cache"));
            parameterMapElement(context.evalNodes("/mapper/parameterMap"));
            resultMapElements(context.evalNodes("/mapper/resultMap"));
            sqlElement(context.evalNodes("/mapper/sql"));
            // 下面要解析 curd 对应的标签
            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);
        }
    }
    
    // 解析 curd 对应的标签
    private void buildStatementFromContext(List<XNode> list) {
        if (configuration.getDatabaseId() != null) {
            buildStatementFromContext(list, configuration.getDatabaseId());
        }
        buildStatementFromContext(list, null);
    }
    // 具体调用解析 curd 标签的方法
    private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
        for (XNode context : list) {
            final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
            try {
                // 解析具体的标签
                statementParser.parseStatementNode();
            } catch (IncompleteElementException e) {
                configuration.addIncompleteStatement(statementParser);
            }
        }
    }
    
    private void cacheElement(XNode context) {
        if (context != null) {
            String type = context.getStringAttribute("type", "PERPETUAL");
            Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
            // 回收策略默认设置 LRU
            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();
            // 这里调用了 MapperBuilderAssistant 的 userNewCache 方法
            builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
        }
    }
    

    下面看一下解析 <cache/> 标签时调用的 useNewCache 方法:

    // MapperBuilderAssistant 的 userNewCache 方法创建 Cache 实例
    public Cache useNewCache(Class<? extends Cache> typeClass,
                             Class<? extends Cache> evictionClass,
                             Long flushInterval,
                             Integer size,
                             boolean readWrite,
                             boolean blocking,
                             Properties props) {
        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();
        configuration.addCache(cache);
        currentCache = cache;
        return cache;
    }
    

    下面是 XMLStatementBuilder 部分代码:

    // 解析 CURD 操作的标签 如 select,update,insert,delete 
    public void parseStatementNode() {
        String id = context.getStringAttribute("id");
        String databaseId = context.getStringAttribute("databaseId");
    
        if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
            return;
        }
    
        String nodeName = context.getNode().getNodeName();
        SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
        // 是否是 select 类型的 sql
        boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
        boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
        // 如果执行 select sql 则缺省值为 true,也就是开启二级缓存
        boolean useCache = context.getBooleanAttribute("useCache", isSelect);
        boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
    
        // Include Fragments before parsing
        XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
        includeParser.applyIncludes(context.getNode());
    
        String parameterType = context.getStringAttribute("parameterType");
        Class<?> parameterTypeClass = resolveClass(parameterType);
    
        String lang = context.getStringAttribute("lang");
        LanguageDriver langDriver = getLanguageDriver(lang);
    
        // Parse selectKey after includes and remove them.
        processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
        // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
        KeyGenerator keyGenerator;
        String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
        keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
        if (configuration.hasKeyGenerator(keyStatementId)) {
            keyGenerator = configuration.getKeyGenerator(keyStatementId);
        } else {
            keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
                                                       configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
                ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
        }
    
        SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
        StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
        Integer fetchSize = context.getIntAttribute("fetchSize");
        Integer timeout = context.getIntAttribute("timeout");
        String parameterMap = context.getStringAttribute("parameterMap");
        String resultType = context.getStringAttribute("resultType");
        Class<?> resultTypeClass = resolveClass(resultType);
        String resultMap = context.getStringAttribute("resultMap");
        String resultSetType = context.getStringAttribute("resultSetType");
        ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
        if (resultSetTypeEnum == null) {
            resultSetTypeEnum = configuration.getDefaultResultSetType();
        }
        String keyProperty = context.getStringAttribute("keyProperty");
        String keyColumn = context.getStringAttribute("keyColumn");
        String resultSets = context.getStringAttribute("resultSets");
    
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
                                            fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
                                            resultSetTypeEnum, flushCache, useCache, resultOrdered,
                                            keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
    }
    

    由上面的源码可知,select 标签是默认开启二级缓存的,但是前提也是需要前两个点(这里指的是序号 1 和 2)。

    开启二级缓存与不开启二级缓存工作模式上的区别:
    mybatis使用与未使用二级缓存的工作模式

    当然未使用二级缓存的话还是有一级缓存的。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值