Mybatis源码解析之二级缓存

前言:

    在上一篇文章Mybatis源码解析之一级缓存中分析了Mybatis一级缓存的使用及源码分析。

    本篇博客就继续从源码角度来分析关于Mybatis的二级缓存。

1.二级缓存使用

    项目配置均同一级缓存的配置相同,不同点就是

    1)在blog.xml中假如<cache/>标签


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 
<mapper namespace="mybatis.BlogMapper">
	<select id="queryById" parameterType="int" resultType="jdbc.Blog">
		select * from blog where id = #{id}
    </select>
    <update id="updateBlog" parameterType="jdbc.Blog">
    	update Blog set name = #{name},url = #{url} where id=#{id}
    </update>
    <!-- 开启BlogMapper二级缓存 -->
    <cache/>
</mapper>

  

2.二级缓存的测试及结论

    1)不同的session进行相同的查询

public static void main(String[] args) {
    SqlSession session = sqlSessionFactory.openSession();
    SqlSession session1 = sqlSessionFactory.openSession();
    try {

        Blog blog = (Blog)session.selectOne("queryById",17);
        Blog blog2 = (Blog)session1.selectOne("queryById",17);

    } finally {
        session.close();
    }
}

    结论:执行两次DB查询

    2)第一个session查询完成之后,手动提交,在执行第二个session查询

public static void main(String[] args) {
    SqlSession session = sqlSessionFactory.openSession();
    SqlSession session1 = sqlSessionFactory.openSession();
    try {

        Blog blog = (Blog)session.selectOne("queryById",17);
        session.commit();

        Blog blog2 = (Blog)session1.selectOne("queryById",17);
    } finally {
        session.close();
    }
}

    结论:执行一次DB查询

    3)第一个session查询完成之后,手动关闭,在执行第二个session查询

public static void main(String[] args) {
    SqlSession session = sqlSessionFactory.openSession();
    SqlSession session1 = sqlSessionFactory.openSession();
    try {

        Blog blog = (Blog)session.selectOne("queryById",17);
        session.close();

        Blog blog2 = (Blog)session1.selectOne("queryById",17);
    } finally {
        session.close();
    }
}

    结论:执行一次DB查询

    

    总结:二级缓存的生效必须在session提交或关闭之后才会生效

3.二级缓存源码分析

    我们看下二级缓存与一级缓存的配置的异同,发现只是在blog.xml中添加了一个<cache/>标签,就实现了二级缓存的功能,那么我们就从这个标签开始分析

    1)<cache/>的解析

    按照之前的对Mybatis的分析,对blog.xml的解析工作主要交给XMLConfigBuilder.parse()方法来实现的

// XMLConfigBuilder.parse()
public Configuration parse() {
    if (parsed) {
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));// 在这里
    return configuration;
}

// parseConfiguration()
// 既然是在blog.xml中添加的,那么我们就直接看关于mappers标签的解析
private void parseConfiguration(XNode root) {
    try {
        Properties settings = settingsAsPropertiess(root.evalNode("settings"));
        //issue #117 read properties first
        propertiesElement(root.evalNode("properties"));
        loadCustomVfs(settings);
        typeAliasesElement(root.evalNode("typeAliases"));
        pluginElement(root.evalNode("plugins"));
        objectFactoryElement(root.evalNode("objectFactory"));
        objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        reflectionFactoryElement(root.evalNode("reflectionFactory"));
        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);
    }
}

// mapperElement()
private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            if ("package".equals(child.getName())) {
                String mapperPackage = child.getStringAttribute("name");
                configuration.addMappers(mapperPackage);
            } else {
                String resource = child.getStringAttribute("resource");
                String url = child.getStringAttribute("url");
                String mapperClass = child.getStringAttribute("class");
                // 按照我们本例的配置,则直接走该if判断
                if (resource != null && url == null && mapperClass == null) {
                    ErrorContext.instance().resource(resource);
                    InputStream inputStream = Resources.getResourceAsStream(resource);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                    // 生成XMLMapperBuilder,并执行其parse方法
                    mapperParser.parse();
                } else if (resource == null && url != null && mapperClass == null) {
                    ErrorContext.instance().resource(url);
                    InputStream inputStream = Resources.getUrlAsStream(url);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
                    mapperParser.parse();
                } else if (resource == null && url == null && mapperClass != null) {
                    Class<?> mapperInterface = Resources.classForName(mapperClass);
                    configuration.addMapper(mapperInterface);
                } else {
                    throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
                }
            }
        }
    }
}

    2)XMLMapperBuilder.parse()解析对应的blog.xml配置文件

// XMLMapperBuilder.parse()
public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
        // 解析mapper属性
        configurationElement(parser.evalNode("/mapper"));
        configuration.addLoadedResource(resource);
        bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingChacheRefs();
    parsePendingStatements();
}

// configurationElement()
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"));
        // 这里会将生成的Cache包装到对应的MappedStatement,这个继续在4)中分析
        buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
    }
}

// cacheElement()
private void cacheElement(XNode context) throws Exception {
    if (context != null) {
        String type = context.getStringAttribute("type", "PERPETUAL");
        Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
        String eviction = context.getStringAttribute("eviction", "LRU");
        Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
        Long flushInterval = context.getLongAttribute("flushInterval");
        Integer size = context.getIntAttribute("size");
        boolean readWrite = !context.getBooleanAttribute("readOnly", false);
        boolean blocking = context.getBooleanAttribute("blocking", false);
        Properties props = context.getChildrenAsProperties();
        // 关键一步在这里,下面接着分析
        builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
}

    3)MapperBuilderAssistant.useNewCache()

// MapperBuilderAssistant.useNewCache()
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)
        .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;
}

    4)buildStatementFromContext(context.evalNodes("select|insert|update|delete"));将Cache包装到MappedStatement

// buildStatementFromContext()
private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
        buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
}

//buildStatementFromContext()
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
        final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
        try {
            // 每一条执行语句转换成一个MappedStatement
            statementParser.parseStatementNode();
        } catch (IncompleteElementException e) {
            configuration.addIncompleteStatement(statementParser);
        }
    }
}

// XMLStatementBuilder.parseStatementNode();
public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");
    ...

    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultType = context.getStringAttribute("resultType");
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    ...
    // 在这里添加MappedStatement
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
                                        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
                                        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
                                        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

// builderAssistant.addMappedStatement()
public MappedStatement addMappedStatement(
    String id,
    ...) {

    if (unresolvedCacheRef) {
        throw new IncompleteElementException("Cache-ref not yet resolved");
    }

    id = applyCurrentNamespace(id, false);
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;

    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        ...
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        .useCache(valueOrDefault(useCache, isSelect))
        .cache(currentCache);// 在这里将之前生成的Cache封装到MappedStatement

    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
        statementBuilder.parameterMap(statementParameterMap);
    }

    MappedStatement statement = statementBuilder.build();
    configuration.addMappedStatement(statement);
    return statement;
}

    总结:有关于<cache/>标签的解析就到这了。我们再来回顾下,到底产生了什么对象,又放在哪里了?

    从前到后依次是:

    * XMLConfigureBuilder解析对应的mapper xml,为每一个mapper xml生成一个XMLMapperBuilder对象

    * XMLMapperBuilder负责具体的解析mapper xml操作

    * 关于其中的cache标签,每一个XMLMapperBuilder有对应的MapperBuilderAssistant对象进行cache标签解析

    * MapperBuilderAssistant负责为cache标签创建Cache对象,并存在到Configuration一份,保存到当前属性currentCache一份

    * XMLMapperBuilder解析mapper xml中的每一个执行语句,将其解析为一个MappedStatement,并将Cache对象封装其中

4.查询时如何使用这个一级缓存?

    我们来看下上面的查询示例

Blog blog = (Blog)session.selectOne("queryById",17);

    1)查询源码分析(根据上面的分析我们直接定位到CachingExecutor.query()方法)

// CachingExecutor.query()
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
    // 该Cache就是一级缓存,对应当前MappedStatement的Cache,
    // 我们在上面解析blog.xml时分析过每一个MappedStatement都有一个Cache对象,就是这里
    Cache cache = ms.getCache();
    if (cache != null) {
        flushCacheIfRequired(ms);
        if (ms.isUseCache() && resultHandler == null) {
            ensureNoOutParams(ms, parameterObject, boundSql);
            @SuppressWarnings("unchecked")
            // 先从缓存中获取该cache对应的值
            List<E> list = (List<E>) tcm.getObject(cache, key);
            if (list == null) {
                // 如果没有值,则执行查询,这个查询实际也是先走一级缓存查询,一级缓存也没有的话,则进行DB查询
                list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                // 然后将值放到tcm中,tcm就是TransactionalCacheManager
                tcm.putObject(cache, key, list); // issue #578 and #116
            }
            return list;
        }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

    2)tcm.putObject(cache, key, list)如果保存值?

// TransactionalCacheManager.putObject(cache, key, list)
public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
}

// getTransactionalCache()
private TransactionalCache getTransactionalCache(Cache cache) {
    TransactionalCache txCache = transactionalCaches.get(cache);
    if (txCache == null) {
        txCache = new TransactionalCache(cache);
        transactionalCaches.put(cache, txCache);
    }
    return txCache;
}

// TransactionalCache.putObject()
@Override
public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
}

    最终将值放到TransactionCache中,下面我们看下TransactionCache的结构

public class TransactionalCache implements Cache {

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

  private Cache delegate;
  private boolean clearOnCommit;
  private Map<Object, Object> entriesToAddOnCommit;
  private Set<Object> entriesMissedInCache;

    可以知道,刚才的值只是存放到entriesToAddOnCommit这个属性中。

    3)tcm.getObject(cache, key)如何查询二级缓存

// TransactionalCacheManager.getObject(cache, key)
public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
}

// TransactionalCache.getObject()
@Override
public Object getObject(Object key) {
    // 真正的问题在这
    // 刚才存储的时候放到了entriesToAddOnCommit这个map中
    // 但是查询的时候是从delegate中去查询的
    Object object = delegate.getObject(key);
    if (object == null) {
        entriesMissedInCache.add(key);
    }
    // issue #146
    if (clearOnCommit) {
        return null;
    } else {
        return object;
    }
}

    总结:刚才存储二级缓存对象的时候放到了TransactionalCache.entriesToAddOnCommit这个map中

    但是查询的时候是从TransactionalCache.delegate中去查询的,所以这个二级缓存查询完成之后是没有立刻生效的

    按照我们刚才的示例,只有SqlSession提交或关闭后才提交,下面我们接着来看下提交或关闭操作做了什么

 2)为何只有SqlSession提交或关闭之后二级缓存才会生效?

    那我们来看下SqlSession.commit()方法做了什么

// DefaultSqlSession.commit()
@Override
public void commit(boolean force) {
    try {
        // 主要是这句
        executor.commit(isCommitOrRollbackRequired(force));
        dirty = false;
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

// CachingExecutor.commit()
@Override
public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    tcm.commit();// 在这里
}

// TransactionalCacheManager.commit()
public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
        txCache.commit();// 在这里
    }
}

// TransactionalCache.commit()
public void commit() {
    if (clearOnCommit) {
        delegate.clear();
    }
    flushPendingEntries();//这一句
    reset();
}

// TransactionalCache.flushPendingEntries()
private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
        // 在这里真正的将entriesToAddOnCommit的对象逐个添加到delegate中,只有这时,二级缓存才真正的生效
        delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
        if (!entriesToAddOnCommit.containsKey(entry)) {
            delegate.putObject(entry, null);
        }
    }
}
发布了124 篇原创文章 · 获赞 126 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/qq_26323323/article/details/85704198