Mybatis缓存原理分析

424 阅读16分钟

你需要下载Mybatis的源码,跟着本篇文章来一起阅读。

缓存源码分析

二级缓存构建在一级缓存之上,在收到查询请求时,mybatis首先会查询二级缓存,若二级缓存未命中,再去查询一级缓存,一级缓存没有,再查询数据库。 二级缓存与一级缓存不同,二级缓存和具体的命名空间绑定,一个Mapper中有一个Cache,相同Mapper中的MappedStatement共用一个Cache,一级缓存则是和SqlSession绑定。

:::tips SqlSession提交或关闭之后二级缓存才会生效。 :::

启动二级缓存:

  1. 开启全局二级缓存配置
    <settings>
        <!--开启全局的二级缓存配置-->
        <setting name="cacheEnabled" value="true"/>
    </settings>
  1. 在需要使用二级缓存的mapper配置文件中配置标签
<cache></cache>
  1. 在具体CURD标签上配置useCache=true
   <select id="findById" resultMap="userMap" useCache="true" >
       select * from user where id = #{id}
   </select>

标签原理解析

标签是在Mapper.xml文件中,所以在解析Mapper.xml中会处理标签。 根据我们之前的分析Mapper.xml的解析会在XMLMapperBuilder 类的parse方法完成

    public void parse() {
        // 判断当前 Mapper 是否已经加载过
        if (!configuration.isResourceLoaded(resource)) {
            // 解析 `<mapper />` 节点
            configurationElement(parser.evalNode("/mapper"));
            // 标记该 Mapper 已经加载过
            configuration.addLoadedResource(resource);
            // 绑定 Mapper
            bindMapperForNamespace();
        }

        // 解析待定的 <resultMap /> 节点
        parsePendingResultMaps();
        // 解析待定的 <cache-ref /> 节点
        parsePendingCacheRefs();
        // 解析待定的 SQL 语句的节点
        parsePendingStatements();
    }

在解析标签的configurationElement方法中,看到了解析<cache/>节点的方法cacheElement

    // 解析 `<mapper />` 节点
    private void configurationElement(XNode context) {
        try {
            // 获得 namespace 属性
            String namespace = context.getStringAttribute("namespace");
            if (namespace == null || namespace.equals("")) {
                throw new BuilderException("Mapper's namespace cannot be empty");
            }
            // 设置 namespace 属性
            builderAssistant.setCurrentNamespace(namespace);
            // 解析 <cache-ref /> 节点 当前的mapper中引入其他缓存时会用到这个标签
            cacheRefElement(context.evalNode("cache-ref"));
            // 解析 <cache /> 节点
            cacheElement(context.evalNode("cache"));
            // 已废弃!老式风格的参数映射。内联参数是首选,这个元素可能在将来被移除,这里不会记录。
            parameterMapElement(context.evalNodes("/mapper/parameterMap"));
            // 解析 <resultMap /> 节点们
            resultMapElements(context.evalNodes("/mapper/resultMap"));
            // 解析 <sql /> 节点们
            sqlElement(context.evalNodes("/mapper/sql"));
            // 解析 <select /> <insert /> <update /> <delete /> 节点们
            // 这里会将生成的Cache包装到对应的MappedStatement
            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);
        }
    }

下面我们看cacheElement方法中做了些什么,其实创建Cache对象

    // 解析 <cache /> 标签
    private void cacheElement(XNode context) throws Exception {
        if (context != null) {
            //解析<cache/>标签的type属性,这里我们可以自定义cache的实现类,比如redisCache,如果没有自定义,这里使用和一级缓存相同的PERPETUAL
            String type = context.getStringAttribute("type", "PERPETUAL");
            Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
            // 获得负责过期的 Cache 实现类
            String eviction = context.getStringAttribute("eviction", "LRU");
            Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
            // 清空缓存的频率。0 代表不清空
            Long flushInterval = context.getLongAttribute("flushInterval");
            // 缓存容器大小
            Integer size = context.getIntAttribute("size");
            // 是否序列化
            boolean readWrite = !context.getBooleanAttribute("readOnly", false);
            // 是否阻塞
            boolean blocking = context.getBooleanAttribute("blocking", false);
            // 获得 Properties 属性
            Properties props = context.getChildrenAsProperties();
            // 创建 Cache 对象
            builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
        }
    }

Cache对象的创建最终调用了MapperBuilderAssistant.useNewCache()方法: 只会解析一次Mapper.xml,也就是会只创建一次Cache对象,并放进Configuration中。

    /**
     * 创建 Cache 对象
     *
     * @param typeClass 负责存储的 Cache 实现类
     * @param evictionClass 负责过期的 Cache 实现类
     * @param flushInterval 清空缓存的频率。0 代表不清空
     * @param size 缓存容器大小
     * @param readWrite 是否序列化
     * @param blocking 是否阻塞
     * @param props Properties 对象
     * @return Cache 对象
     */
    public Cache useNewCache(Class<? extends Cache> typeClass,
                             Class<? extends Cache> evictionClass,
                             Long flushInterval,
                             Integer size,
                             boolean readWrite,
                             boolean blocking,
                             Properties props) {

        // 1.生成Cache对象
        Cache cache = new CacheBuilder(currentNamespace)
                //这里如果我们定义了<cache/>中的type,就使用自定义的Cache,否则使用和一级缓存相同的PerpetualCache
                .implementation(valueOrDefault(typeClass, PerpetualCache.class))
                .addDecorator(valueOrDefault(evictionClass, LruCache.class))
                .clearInterval(flushInterval)
                .size(size)
                .readWrite(readWrite)
                .blocking(blocking)
                .properties(props)
                .build();
        // 2.添加到Configuration中
        configuration.addCache(cache);
        // 3.并将cache赋值给MapperBuilderAssistant.currentCache
        currentCache = cache;
        return cache;
    }

OK,代码执行到这里我们知道了Cache对象已经存储到了Configuration中,并且并将cache赋值给MapperBuilderAssistant.currentCache。 我们继续回到解析节点的方法XMLMapperBuilder.configurationElement

    // 解析 `<mapper />` 节点
    private void configurationElement(XNode context) {
        try {
            // 获得 namespace 属性
            String namespace = context.getStringAttribute("namespace");
            if (namespace == null || namespace.equals("")) {
                throw new BuilderException("Mapper's namespace cannot be empty");
            }
            // 设置 namespace 属性
            builderAssistant.setCurrentNamespace(namespace);
            // 解析 <cache-ref /> 节点 当前的mapper中引入其他缓存时会用到这个标签
            cacheRefElement(context.evalNode("cache-ref"));
            // 解析 <cache /> 节点
            cacheElement(context.evalNode("cache"));
            // 已废弃!老式风格的参数映射。内联参数是首选,这个元素可能在将来被移除,这里不会记录。
            parameterMapElement(context.evalNodes("/mapper/parameterMap"));
            // 解析 <resultMap /> 节点们
            resultMapElements(context.evalNodes("/mapper/resultMap"));
            // 解析 <sql /> 节点们
            sqlElement(context.evalNodes("/mapper/sql"));
            // 解析 <select /> <insert /> <update /> <delete /> 节点们
            // 这里会将生成的Cache包装到对应的MappedStatement
            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);
        }
    }

最终会调用到buildStatementFromContext将Cache包装到MappedStatement中,该方法最终会调用到 XMLStatementBuilder.parseStatementNode()方法解析每一个StatementNode,最终会调用addMappedStatement方法创建MappedStatement并且添加到Configuration中

    /**
     * 执行解析
     */
    public void parseStatementNode() {
        //....获取各种属性
        //清空二级缓存默认值为false
        boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
        //使用二级缓存 默认值为true
        boolean useCache = context.getBooleanAttribute("useCache", isSelect);
        //.....
        // 创建 MappedStatement 对象
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
                fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
                resultSetTypeEnum, flushCache, useCache, resultOrdered,
                keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
    }

需要注意的是addMappedStatement方法是MapperBuilderAssistant类中的一个方法,而在上述的解析标签中会创建Cache对象,并且会将该对象赋值给MapperBuilderAssistant.currentCache属性中。 如下代码,可以清楚的看到将currentCache赋值给MappedStatement对象中。

    // 构建 MappedStatement 对象
    public MappedStatement addMappedStatement(......) {
        // 如果只想的 Cache 未解析,抛出 IncompleteElementException 异常
        if (unresolvedCacheRef) {
            throw new IncompleteElementException("Cache-ref not yet resolved");
        }
        // 获得 id 编号,格式为 `${namespace}.${id}`
        id = applyCurrentNamespace(id, false);
        boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
        // 创建 MappedStatement.Builder 对象
        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)) // 获得 ResultMap 集合
                .resultSetType(resultSetType)
                .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
                .useCache(valueOrDefault(useCache, isSelect))
                .cache(currentCache); // 在这里将之前生成的Cache对象封装到MappedStatement
        // 获得 ParameterMap ,并设置到 MappedStatement.Builder 中
        ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
        if (statementParameterMap != null) {
            statementBuilder.parameterMap(statementParameterMap);
        }
        // 创建 MappedStatement 对象
        MappedStatement statement = statementBuilder.build();
        // 添加到 configuration 中
        configuration.addMappedStatement(statement);
        return statement;
    }

:::tips 注意:在解析Mapper.xml会生成一个Cache对象,之后解析NodeStatement:select | insert| update | delete标签,并且每个标签会为其生成一个MappedStatement对象,并且将之前生成的Cache对象封装到MappedStatement中,也就是说一个Mapper.xml对应着一个Cache对象。这也就是为什么二级缓存作用域是namespce。 :::

需要注意,当我们开启二级缓存后使用的执行器是CachingExecutor,我们断点在执行器的位置可以看到具体的执行器的实例 image.png 通过上述的截图我们还可以看到在CachingExecutor的构造方法中传递了一个delegate被委托的Executor对象SimpleExecutor

CachingExecutor源码分析

下面我们看缓存执行的query方法的实现: 可以看到主要经历了几个步骤:

  1. 从MappedStatement中获取缓存对象,完美对接了我们上述的分析思路
  2. 判断cache对象是否为空
    1. 如果为空通过delegate(SimpleExecutor)执行query方法,如果有一级缓存则走一级缓存查询,没有一级缓存则查询数据库
    2. 如果不为空继续往下走
  3. 如果设置了flushCache=true 则清空缓存
  4. 如果useCache = true 则获取缓存
    1. 如果获取结果为空则delegate.query 如果有一级缓存则走一级缓存查询,如果没有一级缓存则查询数据库
    2. 如果获取结果不为空,则返回结果即可 :::tips 证实:通过源码证实了,会先走二级缓存,如果没有二级缓存则走一级缓存,如果没有一级缓存则走数据库查询。 :::
   @Override
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
            throws SQLException {

        // 从 MappedStatement 中获取 Cache,注意这里的 Cache 是从MappedStatement中获取的
        // 也就是我们上面解析Mapper中<cache/>标签中创建的,它保存在Configration中
        // 我们在初始化解析xml时分析过每一个MappedStatement都有一个Cache对象,就是这里
        Cache cache = ms.getCache();

        // 如果配置文件中没有配置 <cache>,则 cache 为空
        if (cache != null) {
            //如果需要刷新缓存的话就刷新:flushCache="true"
            flushCacheIfRequired(ms);
            if (ms.isUseCache() && resultHandler == null) {
                // 暂时忽略,存储过程相关
                ensureNoOutParams(ms, boundSql);
                @SuppressWarnings("unchecked")
                // 从二级缓存中,获取结果,这里为什么使用的tcm获取缓存呢?
                List<E> list = (List<E>) tcm.getObject(cache, key);
                if (list == null) {
                    // 如果没有值,则执行查询,这个查询实际也是先走一级缓存查询,一级缓存也没有的话,则进行DB查询
                    list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    // 缓存查询结果 并不是真正的向二级缓存存储数据
                    tcm.putObject(cache, key, list); // issue #578 and #116
                }
                // 如果存在,则直接返回结果
                return list;
            }
        }
        // 不使用缓存,则从数据库中查询(会查一级缓存)
        return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }
  /**
     * 如果需要清空缓存,则进行清空
     *
     * @param ms MappedStatement 对象
     */
    private void flushCacheIfRequired(MappedStatement ms) {
        Cache cache = ms.getCache();
        if (cache != null && ms.isFlushCacheRequired()) { // 是否需要清空缓存
            //清空二级缓存
            tcm.clear(cache);
        }

通过看源码不知道大家有么有发现,无论是获取缓存还是清空缓存都会通过tcm处理而不是通过Cache对象直接处理,这是为什么呢?其实做后台开发我们都需要考虑并发的情况,其实java几乎所有的框架都会考虑并发问题,由于 MappedStatement 存在于全局配置中,可以多个 CachingExecutor 获取到,这样就会出现线程安全问题。除此之外,若不加以控制,多个 事务共⽤⼀个缓存实例,会导致脏读问题。⾄于脏读问题,需要借助其他类来处理,也就是上⾯代码中tcm 变量对应的类型。

原来如此,试想一下并发情况下访问同一个Mapper.xml中的方法,它使用同一个Cache对象,如果不加以控制确实会出现并发问题。

下面我们来看一下tcm它是如何处理并发问题的。tcm就是TransactionalCacheManager的实例,具体实现如下:

/**
 * {@link TransactionalCache} 事务缓存管理器
 *
 * @author Clinton Begin
 */
public class TransactionalCacheManager {

    /**
     * // Cache 与 TransactionalCache 的映射关系表
     */
    private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();

    /**
     * 清空缓存
     *
     * @param cache Cache 对象
     */
    public void clear(Cache cache) {
        getTransactionalCache(cache).clear();
    }

    /**
     * 获得缓存中,指定 Cache + K 的值。
     *
     * @param cache Cache 对象
     * @param key   键
     * @return 值
     */
    public Object getObject(Cache cache, CacheKey key) {
        // 直接从TransactionalCache中获取缓存
        return getTransactionalCache(cache).getObject(key);
    }

    /**
     * 添加 Cache + KV ,到缓存中
     *
     * @param cache Cache 对象
     * @param key   键
     * @param value 值
     */
    public void putObject(Cache cache, CacheKey key, Object value) {
        // 直接存入TransactionalCache的缓存中
        getTransactionalCache(cache).putObject(key, value);
    }

    /**
     * 提交所有 TransactionalCache
     */
    public void commit() {
        for (TransactionalCache txCache : transactionalCaches.values()) {
            txCache.commit();
        }
    }

    /**
     * 回滚所有 TransactionalCache
     */
    public void rollback() {
        for (TransactionalCache txCache : transactionalCaches.values()) {
            txCache.rollback();
        }
    }

    /**
     * 获得 Cache 对应的 TransactionalCache 对象
     *
     * @param cache Cache 对象
     * @return TransactionalCache 对象
     */
    private TransactionalCache getTransactionalCache(Cache cache) {
        //如果 key 对应的 value 不存在,则使用获取 remappingFunction 重新计算后的值,并保存为该 key 的 value,否则返回 value。
        return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);
    }

}

从上述代码可以看出,TransactionalCacheManager内部维护了Cache实例与TransactionalCache实例之间的关系,该类仅仅是负责维护两者的映射关系,真正做事的是TransactionalCache是一种缓存装饰器,可以为Cache实例增加事务功能. 如下代码:

  1. 获取缓存getObject:直接从delegate代理对象中去查询也就是真实的缓存对象查询,如果不存在添加到缓存未命中集合中entriesMissedInCache
  2. 存储缓存putObject: 主要到这个方法只是把它存入到了entriesToAddOnCommit 集合中并没有存储Cache对象中,这一步是关键,缓存并没有立刻生效,如果直接存到delegate会导致脏数据的问题。
  3. 缓存生效commit:当sqlsession调用commit方法的时候,才会将entriesToAddOnCommit中的数据存入到delegate中
  4. clear方法:会将clearOnCommit置为true,并且清空entriesToAddOnCommit当再次调用commit方法如果clearOnCommit==true就会清空缓存
public class TransactionalCache implements Cache {

    private static final Log log = LogFactory.getLog(TransactionalCache.class);

    /**
     * 委托的 Cache 对象。
     *
     * 实际上,就是二级缓存 Cache 对象。
     */
    private final Cache delegate;
    /**
     * 提交时,清空 {@link #delegate}
     *
     * 初始时,该值为 false
     * 清理后{@link #clear()} 时,该值为 true ,表示持续处于清空状态
     */
    private boolean clearOnCommit;
    /**
     *  // 在事务被提交前,所有从数据库中查询的结果将缓存在此集合中
     */
    private final Map<Object, Object> entriesToAddOnCommit;
    /**
     *   在事务被提交前,当缓存未命中时,CacheKey 将会被存储在此集合中
     */
    private final Set<Object> entriesMissedInCache;

    public TransactionalCache(Cache delegate) {
        this.delegate = delegate;
        this.clearOnCommit = false;
        this.entriesToAddOnCommit = new HashMap<>();
        this.entriesMissedInCache = new HashSet<>();
    }

    @Override
    public String getId() {
        return delegate.getId();
    }

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

    @Override
    public Object getObject(Object key) {
        // 查询的时候是直接从delegate中去查询的,也就是从真正的缓存对象中查询
        Object object = delegate.getObject(key);
        // 如果不存在,则添加到 entriesMissedInCache 中
        if (object == null) {
            // 缓存未命中,则将 key 存入到 entriesMissedInCache 中
            entriesMissedInCache.add(key);
        }
        // issue #146
        // 如果 clearOnCommit 为 true ,表示处于持续清空状态,则返回 null
        if (clearOnCommit) {
            return null;
        // 返回 value
        } else {
            return object;
        }
    }

    @Override
    public ReadWriteLock getReadWriteLock() {
        return null;
    }

    @Override
    public void putObject(Object key, Object object) {
        // 将键值对存入到 entriesToAddOnCommit 这个Map中中,而非真实的缓存对象 delegate 中
        entriesToAddOnCommit.put(key, object);
    }

    @Override
    public Object removeObject(Object key) {
        return null;
    }

    @Override
    public void clear() {
        // 标记 clearOnCommit 为 true
        clearOnCommit = true;
        // 清空 entriesToAddOnCommit
        entriesToAddOnCommit.clear();
    }

    public void commit() {
        // 如果 clearOnCommit 为 true ,则清空 delegate 缓存
        if (clearOnCommit) {
            delegate.clear();
        }
        // 将 entriesToAddOnCommit、entriesMissedInCache 刷入 delegate(cache) 中
        flushPendingEntries();
        // 重置
        reset();
    }

    public void rollback() {
        // 从 delegate 移除出 entriesMissedInCache
        unlockMissedEntries();
        // 重置
        reset();
    }

    private void reset() {
        // 重置 clearOnCommit 为 false
        clearOnCommit = false;
        // 清空 entriesToAddOnCommit、entriesMissedInCache
        entriesToAddOnCommit.clear();
        entriesMissedInCache.clear();
    }
        /**
     * 将 entriesToAddOnCommit、entriesMissedInCache 刷入 delegate 中
     */
    private void flushPendingEntries() {
        // 将 entriesToAddOnCommit 中的内容转存到 delegate 中
        for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {

            // 在这里真正的将entriesToAddOnCommit的对象逐个添加到delegate中,只有这时,二级缓存才真正的生效
            delegate.putObject(entry.getKey(), entry.getValue());
        }
        // 将 entriesMissedInCache 刷入 delegate 中
        for (Object entry : entriesMissedInCache) {
            if (!entriesToAddOnCommit.containsKey(entry)) {
                delegate.putObject(entry, null);
            }
        }
    }

}

当调用sqlsession.update方法,缓存会如何处理?

我们来看CachingExecutor的update方法:

    @Override
    public int update(MappedStatement ms, Object parameterObject) throws SQLException {
        // 如果需要清空缓存,则进行清空
        flushCacheIfRequired(ms);
        // 执行 delegate 对应的方法
        return delegate.update(ms, parameterObject);
    }
  /**
     * 如果需要清空缓存,则进行清空
     *
     * @param ms MappedStatement 对象
     */
    private void flushCacheIfRequired(MappedStatement ms) {
        Cache cache = ms.getCache();
        if (cache != null && ms.isFlushCacheRequired()) { // 是否需要清空缓存
            //清空二级缓存
            tcm.clear(cache);
        }
    }

如上述代码在进行SQL的增、删、改操作时都会清空二级缓存,因此二级缓存不适用于经常进行更新的数据。

二级缓存的设计上大量的运用了装饰者模式,如CachingExecutor,以及各种Cache接口的装饰器

  • 二级缓存实现了SqlSession之间的缓存数据共享,属于namespace级别
  • 二级缓存具有丰富的缓存策略
  • 二级缓存可由多个装饰器,与基础缓存组合而成
  • 二级缓存工作由一个缓存装饰器执行CachingExecutor和一个事务型预缓存TransactionalCache完成

延迟加载源码分析

mybatis为我们提供了延迟加载策略,那么什么是延迟加载呢?就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称为懒加载

  • 优点:先从单表查询,需要时再从关联表中去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张白表速度要快。
  • 缺点:因为只有当需要用到数据时,才会进行数据库查询,这样大批量数据查询时,因为查询工作也要消耗时间,所以可能造成用户等待时间变长,造成用户体验下降。
  • 在多表中:一对多,多对多通常情况下采用延迟加载,其他情况通常采用立即加载。
  • 延迟加载是基于嵌套查询来实现的

局部的延迟加载 在associationcollection标签中都有一个fetchType属性,通过修改它的值,可以修改局部的加载策略

    <resultMap id="userMap" type="com.lagou.pojo.User">
        <id property="id" column="id"></id>
        <result property="username" column="username"></result>

        <!-- fetchType 延迟加载策略
            lazy : 懒加载策略
            eager : 立即加载策略
         -->
        <collection property="orderList" ofType="com.lagou.pojo.Order"
                    select="com.lagou.mapper.IOrderMapper.findOrderByUid" column="id"
                    fetchType="lazy">

            <id property="id" column="oid"/>
            <result property="orderTime" column="ordertime"/>
            <result property="total" column="total"/>
        </collection>
    </resultMap>

   <select id="findAll" resultMap="userMap" >
       select u.*,o.id oid,o.ordertime,o.total,o.uid from user u left join orders o on o.uid = u.id
   </select>

全局延迟加载 在mybatis的核心配置文件中可以使用setting标签修改全局的加载策略

    <settings>
        <!-- 开启全局延迟加载 默认为false -->
        <setting name="lazyLoadingEnabled" value="true"/>
    </settings>

一级缓存原理

至于一级缓存的实现非常简单,在BaseExecutor的类中,具体核心代码如下: 其实就是声明了一个变量,在commit和clear的时候包括执行update方法都会清空缓存

public abstract class BaseExecutor implements Executor {
        /**
     * 本地缓存,即一级缓存
     */
    protected PerpetualCache localCache;
    
    @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());
        // 已经关闭,则抛出 ExecutorException 异常
        if (closed) {
            throw new ExecutorException("Executor was closed.");
        }
        // 清空本地缓存,如果 queryStack 为零,并且要求清空本地缓存。
        if (queryStack == 0 && ms.isFlushCacheRequired()) {
            clearLocalCache();
        }
        List<E> list;
        try {
            // queryStack + 1
            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 - 1
            queryStack--;
        }
        if (queryStack == 0) {
            // 执行延迟加载
            for (DeferredLoad deferredLoad : deferredLoads) {
                deferredLoad.load();
            }
            // issue #601
            // 清空 deferredLoads
            deferredLoads.clear();
            // 如果缓存级别是 LocalCacheScope.STATEMENT ,则进行清理
            if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                // issue #482
                clearLocalCache();
            }
        }
        return list;
    }
        @Override
    public int update(MappedStatement ms, Object parameter) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
        // 已经关闭,则抛出 ExecutorException 异常
        if (closed) {
            throw new ExecutorException("Executor was closed.");
        }
        // 清空本地缓存
        clearLocalCache();
        // 执行写操作
        return doUpdate(ms, parameter);
    }
        @Override
    public void commit(boolean required) throws SQLException {
        // 已经关闭,则抛出 ExecutorException 异常
        if (closed) {
            throw new ExecutorException("Cannot commit, transaction is already closed");
        }
        // 清空本地缓存
        clearLocalCache();
        // 刷入批处理语句
        flushStatements();
        // 是否要求提交事务。如果是,则提交事务。
        if (required) {
            transaction.commit();
        }
    }
    
        @Override
    public void clearLocalCache() {
        if (!closed) {
            // 清理 localCache
            localCache.clear();
            // 清理 localOutputParameterCache
            localOutputParameterCache.clear();
        }
    }

    
        @Override
    public void close(boolean forceRollback) {
        try {
            // 回滚事务
            try {
                rollback(forceRollback);
            } finally {
                // 关闭事务
                if (transaction != null) {
                    transaction.close();
                }
            }
        } catch (SQLException e) {
            // Ignore.  There's nothing that can be done at this point.
            log.warn("Unexpected exception on closing transaction.  Cause: " + e);
        } finally {
            // 置空变量
            transaction = null;
            deferredLoads = null;
            localCache = null;
            localOutputParameterCache = null;
            closed = true;
        }
    }
    
}

延迟加载原理实现

它的原理是,使⽤ CGLIB 或 Javassist( 默认 ) 创建⽬标对象的代理对象。当调⽤代理对象的延迟加载属 性的 getting ⽅法时,进⼊拦截器⽅法。⽐如调⽤a.getB().getName()⽅法,进⼊拦截器的 invoke(...)⽅法,发现a.getB()需要延迟加载时,那么就会单独发送事先保存好的查询关联 B 对象的 SQL ,把 B 查询上来,然后调⽤a.setB(b)⽅法,于是a对象b属性就有值了,接着完 成a.getB().getName()⽅法的调⽤。这就是延迟加载的基本原理。 总结:延迟加载主要是通过动态代理的形式实现,通过代理拦截到指定⽅法,执⾏数据加载。

具体的源码位置在DefaultResultSetHandler在处理结果集的类 Mybatis的查询结果是由ResultSetHandler接⼝的handleResultSets()⽅法处理的。ResultSetHandler 接⼝只有⼀个实现,DefaultResultSetHandler,接下来看下延迟加载相关的⼀个核⼼的⽅法。 如下代码,会判断是否开启延迟加载,在找到嵌套查询的标签会调用configuration.getProxyFactory()获取结果的代理对象resultObject,并返回。

    // 创建映射后的结果对象
    private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
        // useConstructorMappings ,表示是否使用构造方法创建该结果对象。此处将其重置
        this.useConstructorMappings = false; // reset previous mapping result
        final List<Class<?>> constructorArgTypes = new ArrayList<>(); // 记录使用的构造方法的参数类型的数组
        final List<Object> constructorArgs = new ArrayList<>(); // 记录使用的构造方法的参数值的数组
        // 创建映射后的结果对象
        Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
        if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
            // 如果有内嵌的查询,并且开启延迟加载,则创建结果对象的代理对象 resultMap中的配置 会找到嵌套查询的标签
            final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
            for (ResultMapping propertyMapping : propertyMappings) {
                // issue gcode #109 && issue #149 判断当前的属性是否设置fetchType="lazy" 属性
                if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
                    // 设置了延迟加载 走如下的方法 构建代理对象
                    resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
                    break;
                }
            }
        }
        // 判断是否使用构造方法创建该结果对象
        this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
        return resultObject;
    }

默认的代理对象时JavassitProxyFactory

    /**
     * 默认的生成代理对象的方式是:JavassistProxyFactory
     */
    protected ProxyFactory proxyFactory = new JavassistProxyFactory();

然后我们再去看看JavassitProxyFactory的具体实现,其中的createProxy()方法会调用到如下的核心逻辑

/**
* 代理对象实现的核心逻辑
*/
private static class EnhancedResultObjectProxyImpl implements MethodHandler {

        private final Class<?> type;
        private final ResultLoaderMap lazyLoader;
        private final boolean aggressive;
        private final Set<String> lazyLoadTriggerMethods;
        private final ObjectFactory objectFactory;
        private final List<Class<?>> constructorArgTypes;
        private final List<Object> constructorArgs;

        private EnhancedResultObjectProxyImpl(Class<?> type, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
            this.type = type;
            this.lazyLoader = lazyLoader;
            this.aggressive = configuration.isAggressiveLazyLoading();
            this.lazyLoadTriggerMethods = configuration.getLazyLoadTriggerMethods();
            this.objectFactory = objectFactory;
            this.constructorArgTypes = constructorArgTypes;
            this.constructorArgs = constructorArgs;
        }

        public static Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
            final Class<?> type = target.getClass();
            // 创建 EnhancedResultObjectProxyImpl 对象
            EnhancedResultObjectProxyImpl callback = new EnhancedResultObjectProxyImpl(type, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
            // 创建代理对象
            Object enhanced = crateProxy(type, callback, constructorArgTypes, constructorArgs);
            // 将 target 的属性,复制到 enhanced 中
            PropertyCopier.copyBeanProperties(type, target, enhanced);
            return enhanced;
        }
     @Override
        public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
            final String methodName = method.getName();
            try {
                synchronized (lazyLoader) {
                    // 忽略 WRITE_REPLACE_METHOD ,和序列化相关
                    if (WRITE_REPLACE_METHOD.equals(methodName)) {
                        Object original;
                        if (constructorArgTypes.isEmpty()) {
                            original = objectFactory.create(type);
                        } else {
                            original = objectFactory.create(type, constructorArgTypes, constructorArgs);
                        }
                        PropertyCopier.copyBeanProperties(type, enhanced, original);
                        if (lazyLoader.size() > 0) {
                            return new JavassistSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs);
                        } else {
                            return original;
                        }
                    } else {
                        //
                        if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
                            // 加载所有延迟加载的属性  aggressive 对应aggressiveLazyLoading属性  判断lazyLoadTriggerMethods属性设置的方法名
                            if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
                                lazyLoader.loadAll();
                            // 如果调用了 setting 方法,则不在使用延迟加载
                            } else if (PropertyNamer.isSetter(methodName)) {
                                final String property = PropertyNamer.methodToProperty(methodName);
                                lazyLoader.remove(property); // 移除
                            // 如果调用了 getting 方法,则执行延迟加载
                            } else if (PropertyNamer.isGetter(methodName)) {
                                final String property = PropertyNamer.methodToProperty(methodName);
                                // 判断该属性是否进行了延迟加载
                                if (lazyLoader.hasLoader(property)) {
                                    // 发送SQL语句进行SQL的执行
                                    lazyLoader.load(property);
                                }
                            }
                        }
                    }
                }
                // 继续执行原方法
                return methodProxy.invoke(enhanced, args);
            } catch (Throwable t) {
                throw ExceptionUtil.unwrapThrowable(t);
            }
        }
    }

lazyLoader.load()最终会调用到LoadPair.load()方法,这个方法负责查询SQL并且将结果通过set方法设置进去,当调用原方法的get时就会取出对应的数据

class LoadPair{
    ....
    public void load(final Object userObject) throws SQLException {
            if (this.metaResultObject == null || this.resultLoader == null) {
                if (this.mappedParameter == null) {
                    throw new ExecutorException("Property [" + this.property + "] cannot be loaded because "
                            + "required parameter of mapped statement ["
                            + this.mappedStatement + "] is not serializable.");
                }

                // 获得 Configuration 对象
                final Configuration config = this.getConfiguration();
                // 获得 MappedStatement 对象
                final MappedStatement ms = config.getMappedStatement(this.mappedStatement);
                if (ms == null) {
                    throw new ExecutorException("Cannot lazy load property [" + this.property
                            + "] of deserialized object [" + userObject.getClass()
                            + "] because configuration does not contain statement ["
                            + this.mappedStatement + "]");
                }

                // 获得对应的 MetaObject 对象
                this.metaResultObject = config.newMetaObject(userObject);
                // 创建 ResultLoader 对象
                this.resultLoader = new ResultLoader(config, new ClosedExecutor(), ms, this.mappedParameter,
                        metaResultObject.getSetterType(this.property), null, null);
            }

            /* We are using a new executor because we may be (and likely are) on a new thread
             * and executors aren't thread safe. (Is this sufficient?)
             *
             * A better approach would be making executors thread safe. */
            if (this.serializationCheck == null) {
                final ResultLoader old = this.resultLoader;
                this.resultLoader = new ResultLoader(old.configuration, new ClosedExecutor(), old.mappedStatement,
                        old.parameterObject, old.targetType, old.cacheKey, old.boundSql);
            }

            this.metaResultObject.setValue(property, this.resultLoader.loadResult());
        }
}   

:::tips 延迟加载主要是通过动态代理的形式实现,通过代理拦截到指定⽅法,执⾏数据加载. :::