Mybatis源码解析之缓存篇

阅读须知

  • Mybatis源码版本:3.4.4
  • 文章中使用/* */注释的方法会做深入分析

前言

本文从源码分析Mybatis一级和二级缓存的应用,进而阐述使用Mybatis缓存时的一些注意事项,建议读者首先去了解一下Mybatis的读写流程,可以看一下笔者对相关流程源码分析的文章。
在介绍Mybatis一级缓存和二级缓存之前,需要首先理解两个概念:
SqlSession: 引用官方文档中对这个接口作用的说明—SqlSession完全包含了面向数据库执行SQL命令所需的所有方法。你可以通过SqlSession实例来直接执行已映射的SQL语句,也可以通过SqlSession得到映射和管理事务。
namespace: 这里提到的namespace指代的是在应用中配置的mapper配置文件中的namespace。eg:<mapper namespace="xxx"></mapper>

一级缓存

SqlSession维度的缓存,也就是每个SqlSession独享的缓存,我们在使用Mybatis的时候,通常会使用SqlSession的getMapper方法获取到映射,eg:UserDao userDao = sqlSession.getMapper(UserDao.class);获取到映射之后执行相关方法进行数据库操作,获取到的映射对象是通过动态代理生成的代理类MapperProxy对象,这部分内容我们在之前的文章中分析过。Mybatis的数据库操作是通过Executor来执行,一级缓存也是通过Executor来维护,每个SqlSession都会持有一个Executor:
这里写图片描述
图中LocalCache就是Mybatis的一级缓存,LocalCache的查询和写入是在Executor内部完成的,BaseExecutor是一个实现了Executor接口的抽象类,通过阅读源码发现,一级缓存LocalCache就是BaseExecutor内部的一个成员变量。

public abstract class BaseExecutor implements Executor {
    protected PerpetualCache localCache;

public class PerpetualCache implements Cache {
    private Map<Object, Object> cache = HashMap<Object, Object>();

一级缓存保存在PerpetualCache维护的一个Map中,下面为一级缓存执行的流程图:
这里写图片描述
从流程图中看到,在执行Executor query动作时会尝试从一级缓存中获取缓存数据,下面为相关源码:
BaseExecutor:

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    /* 创建缓存key */
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    // 执行查询
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

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()); // MappedStatement id
    cacheKey.update(rowBounds.getOffset()); // offset
    cacheKey.update(rowBounds.getLimit()); // limit
    cacheKey.update(boundSql.getSql()); // sql
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    for (ParameterMapping parameterMapping : parameterMappings) {
        if (parameterMapping.getMode() != ParameterMode.OUT) {
            Object value;
            String propertyName = parameterMapping.getProperty();
            if (boundSql.hasAdditionalParameter(propertyName)) {
                value = boundSql.getAdditionalParameter(propertyName);
            } else if (parameterObject == null) {
                value = null;
            } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                value = parameterObject;
            } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                value = metaObject.getValue(propertyName);
            }
            cacheKey.update(value); // param
        }
    }
    if (configuration.getEnvironment() != null) {
        // Environment id
        cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
}

在BaseExecutor的query方法中发现了缓存key的构建过程,Mybatis用CacheKey这个对象作为缓存的key,上文中说到一级缓存最终会保存在PerpetualCache维护的一个Map中,我们知道,Map用hashcode和equals来确定对象的唯一性,在CacheKey的update方法中,我们发现了hashcode的运算:

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

update方法会计算hashcode和checksum并将参数对象保存到了updateList中,而在CacheKey的equals方法中,除去hashcode、checksum和count的比较外,只要updatelist中的元素一一对应相等,那么就可以认为是相等:
CacheKey:

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

所以,上面createCacheKey方法中调用CacheKey的update方法时传入的元素,就是Mybatis用来确定缓存唯一性的元素。继续分析上文中提到的query方法中调用的重载query方法:
BaseExecutor:

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.");
    }
    // flushCache可以在mapper配置文件中配置
    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 {
            // 缓存为null则查询数据库
            list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
        }
    } finally {
        queryStack--;
    }
    if (queryStack == 0) {
        for (DeferredLoad deferredLoad : deferredLoads) {
            deferredLoad.load();
        }
        deferredLoads.clear();
        // localCacheScope可以配置
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            clearLocalCache(); // 清空一级缓存
        }
    }
    return list;
}

在query方法中发现了清空一级缓存的两处代码,这两处的判断在Mybatis中都是可以配置的。可以在mapper配置文件中配置flushCache来控制数据库操作是否要清空缓存,select默认为false,insert、update、delete默认为true,注意,这个配置一级缓存和二级缓存都会生效,下文会介绍二级缓存。可以使用localCacheScope选项来控制一级缓存的范围,默认为SESSION,会缓存一个SqlSession中的所有查询,如果设置为STATEMENT,则查询会清除缓存。在查询数据库queryFromDatabase方法中,Mybatis做数据库查询操作,并将返回的结果存入一级缓存:
BaseExecutor:

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
        list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
        localCache.removeObject(key);
    }
    localCache.putObject(key, list); // 将查询结果放入一级缓存
    if (ms.getStatementType() == StatementType.CALLABLE) {
        localOutputParameterCache.putObject(key, parameter);
    }
    return list;
}

数据库的写操作会清除缓存, 我们在分析Mybatis写操作流程的源码时看到insert、update、delete都会统一走update方法:
BaseExecutor:

public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (closed) {
        throw new ExecutorException("Executor was closed.");
    }
    clearLocalCache(); // 清空一级缓存
    return doUpdate(ms, parameter);
}

二级缓存

二级缓存是namespace维度的缓存。上文提到SqlSession中会持有一个Executor,在构建SqlSession的时候,Mybatis会根据cacheEnable选项来确定是否使用一个缓存的Executor,也就是CachingExecutor。cacheEnable可以配置,默认为true:
这里写图片描述
Configuration:

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
        executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
        executor = new ReuseExecutor(this, transaction);
    } else {
        executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
        // 如果cacheEnabled配置为true,则用CachingExecutor封装executor
        executor = new CachingExecutor(executor);
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

cacheEnable开启的状态下在mapper配置文件中配置<cache/><cache-ref/>可以控制缓存策略,介绍一级缓存的时候我们提到了可以使用flushCache选项来控制是否清空缓存,这个配置同样会作用于二级缓存。mybatis在解析配置文件的时候,会将<cache/>配置的参数解析成Cache对象保存到MappedStatement(可以理解为mapper配置文件的java代码实现,对象中保存了在mapper配置文件中配置的选项)中,解析配置文件的过程我们已经分析过。构建好SqlSession后,数据库操作就会委托给CachingExcutor进行执行:
CachingExecutor:

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
    Cache cache = ms.getCache(); // 获取缓存对象,也就是上文提到的mapper配置文件中的<cache/>
    if (cache != null) {
        flushCacheIfRequired(ms); /* 如果配置了flushCache=true,则刷新缓存 */
        if (ms.isUseCache() && resultHandler == null) {
            ensureNoOutParams(ms, parameterObject, boundSql);
            // 尝试从二级缓存中获取
            @SuppressWarnings("unchecked")
            List<E> list = (List<E>) tcm.getObject(cache, key);
            if (list == null) {
                // 如果缓存为null则走上面一级缓存的流程
                list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                  tcm.putObject(cache, key, list); // 记录本次查询将要新增的缓存
              }
              return list;
          }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

这是一个典型的按需加载缓存的方式,注意,tcm.putObject方法执行完之后缓存并没有真正的生效,这里只是记录了这次查询将要产生缓存变更,这时候相同的sql查询缓存是不会生效的。同样的,写操作也不是马上会清除缓存:
CachingExecutor:

public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    flushCacheIfRequired(ms); /* 在默认配置下,写操作会刷新缓存 */
    return delegate.update(ms, parameterObject);
}

CachingExecutor:

private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    if (cache != null && ms.isFlushCacheRequired()) {
        /* 缓存管理器清空缓存 */      
        tcm.clear(cache);
    }
}

TransactionalCacheManager:

public void clear(Cache cache) {
    /* 获取TransactionalCache,清空缓存 */
    getTransactionalCache(cache).clear();
}

TransactionalCacheManager:

private TransactionalCache getTransactionalCache(Cache cache) {
    TransactionalCache txCache = transactionalCaches.get(cache);
    if (txCache == null) {
        // 这里构造TransactionalCache对象时传入的Cache对象在做commit操作时会操作这个Cache独享
        txCache = new TransactionalCache(cache);
        transactionalCaches.put(cache, txCache);
    }
    return txCache;
}

TransactionalCache:

public void clear() {
    clearOnCommit = true; // 提交时清理缓存的标记
    entriesToAddOnCommit.clear(); // 清空将要加入到缓存中的对象
}

在执行SqlSession的commit方法之后,缓存的变更会真正的被刷新到缓存中去,开始真正的发挥作用:
DefaultSqlSession:

public void commit(boolean force) {
    try {
        /* 执行commit */
        executor.commit(isCommitOrRollbackRequired(force));
        dirty = false;
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

CachingExecutor:

public void commit(boolean required) throws SQLException {
    delegate.commit(required); // 执行BaseExecutor的commit方法,用于提交事务、清理一级缓存、刷新Statement
    tcm.commit(); /* 缓存管理器提交 */
}

TransactionalCacheManager:

public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
        /* 遍历管理的所有TransactionalCache,执行commit操作 */
        txCache.commit();
    }
}

TransactionalCache:

public void commit() {
    if (clearOnCommit) {
        // 如果设置了提交时清空缓存则进行清空操作
        delegate.clear();
    }
    /* 将暂时保存的缓存真正刷新到缓存中去 */
    flushPendingEntries();
    reset();
}

TransactionalCache:

private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
        delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
        if (!entriesToAddOnCommit.containsKey(entry)) {
            delegate.putObject(entry, null);
        }
    }
}

我们看到在保存缓存和刷新缓存时用到的delegate对象就是上文中提到的构造TransactionalCache对象时传入的Cache对象了,在构建Cache对象时,Mybatis采用装饰者模式为Cache装饰了不同的功能,我们在之前的文章中已经分析过相关内容。缓存保存在PerpetualCache中:

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

PerpetualCache:

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

cache就是一个HashMap对象:
PerpetualCache:

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

到这里,整个Mybatis缓存的源码分析就完成了。

缓存使用注意事项

在介绍一级缓存时我们提到Mybatis的一级缓存是SqlSession级别的缓存,不同的SqlSession之间缓存是不共享的,如果两个SqlSession操作同一张表,这时候就可能出现其中一个SqlSession获取到的数据是过期的,我们在使用这个SqlSession查询就有可能读取过期的脏数据。

在介绍二级缓存时我们提到二级缓存是namespace维度的缓存,全局共享整个namespace的缓存,当我们把针对同一张表的sql操作写到两个不同的mapper文件中或者使用表关联查询时,就很容易出现两个mapper中查询出来的同一条数据不一致的情况。所以在实际应用中,我们建议将cacheEnable设置为false、localCacheScope设置为STATEMENT,不使用mybatis的缓存,需要缓存的时候在应用程序中进行缓存的控制。

关于MyBatis缓存不一致的测试,详见:https://blog.csdn.net/heroqiang/article/details/85339834。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值