MyBatis3源码深度解析(十七)MyBatis缓存(一)一级缓存和二级缓存的实现原理

文章目录

  • 前言
  • [第六章 MyBatis缓存](#第六章 MyBatis缓存)
    • [6.1 MyBatis缓存实现类](#6.1 MyBatis缓存实现类)
    • [6.2 MyBatis一级缓存实现原理](#6.2 MyBatis一级缓存实现原理)
      • [6.2.1 一级缓存在查询时的使用](#6.2.1 一级缓存在查询时的使用)
      • [6.2.2 一级缓存在更新时的清空](#6.2.2 一级缓存在更新时的清空)
    • [6.3 MyBatis二级缓存的实现原理](#6.3 MyBatis二级缓存的实现原理)
      • [6.3.1 实现的二级缓存的Executor类型](#6.3.1 实现的二级缓存的Executor类型)
      • [6.3.2 二级缓存在查询时使用](#6.3.2 二级缓存在查询时使用)
      • [6.3.3 二级缓存在更新时清空](#6.3.3 二级缓存在更新时清空)

前言

缓存是MyBatis中非常重要的特性。合理使用缓存,可以减少数据库IO,显著提升系统性能;但在分布式环境下,如果使用不当则会带来数据一致性问题。

在上一节【MyBatis3源码深度解析(十六)SqlSession的创建与执行(三)Mapper方法的调用过程】中提到,MyBatis提供了一级缓存和二级缓存来提升查询效率,一级缓存在BaseExecutor类中完成,二级缓存在CachingExecutor类中完成。

第六章 MyBatis缓存

6.1 MyBatis缓存实现类

MyBatis缓存基于JVM堆内存实现,即所有的缓存数据都存放在Java对象中。 MyBatis通过Cache接口定义缓存对象的行为,其定义如下:

java 复制代码
源码1:org.apache.ibatis.cache.Cache

public interface Cache {
    // 获取缓存ID
    String getId();
    // 将一个Java对象添加到缓存中
    void putObject(Object key, Object value);
    // 根据key获取一个缓存对象
    Object getObject(Object key);
    // 根据key移除一个缓存对象
    Object removeObject(Object key);
    // 清空缓存
    void clear();
    // 获取缓存中存放的数据数量
    int getSize();
    // 3.2.6版本后不再使用
    default ReadWriteLock getReadWriteLock() {
        return null;
    }
}

Cache接口采用装饰器模式设计。它有一个基本的实现类PerpetualCache。

java 复制代码
源码2:org.apache.ibatis.cache.impl.PerpetualCache

public class PerpetualCache implements Cache {
    
    private final String id;
    // 内部维护了一个HashMap容器以保存缓存数据
    private final 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;
        // 当两个缓存对象的ID相同时,即认为缓存对象相同
        return getId().equals(otherCache.getId());
    }
    
    @Override
    public int hashCode() {
        if (getId() == null) {
            throw new CacheException("Cache instances require an ID.");
        }
        // 仅以缓存对象的ID作为因子生成hashCode
        return getId().hashCode();
    }
}

由 源码2 可知,PerpetualCache的实现非常简单,内部仅仅维护了一个HashMap实例存放缓存对象。

需要注意的是,PerpetualCache类重写了Object类的equals()方法和hashCode()方法。由equals()方法可知,当两个缓存对象的ID相同时,即认为缓存对象相同;由hashCode()方法可知,仅以缓存对象的ID作为因子生成hashCode。

除了基础的PerpetualCache实现类,MyBatis还提供了许多其他的实现,对PerpetualCache类的功能进行增强。借助IDE,可以列出Cache的全部实现类:

MyBatis的一级缓存,使用的是PerpetualCache;二级缓存使用的是TransactionalCache。

6.2 MyBatis一级缓存实现原理

MyBatis一级缓存默认是开启的,而且不能关闭。

至于一级缓存不能关闭的原因,MyBatis核心开发人员做出了解释:MyBatis的一些关键特性(例如通过<association>和<collextion>建立级联映射、避免循环引用(circular references)、加速重复嵌套查询等)都是基于MyBatis一级缓存实现的,而且MyBatis结果集映射相关代码重度依赖CacheKey,所以MyBatis一级缓存不支持关闭。

在MyBatis主配置文件中,有这样一个配置属性:<setting name="localCacheScope" value="SESSION"/>,用于控制一级缓存的级别。该属性的取值为SESSION、STATEMENT。

当指定localCacheScope参数值为SESSION时,缓存对整个SqlSession有效,只有执行DML语句(更新语句)时,缓存才会被清除;当指定localCacheScope参数值为STATEMENT时,缓存仅对当前执行的SQL语句有效,当语句执行完毕后,缓存就会被清除。

前面提到,一级缓存在BaseExecutor类中完成:

java 复制代码
源码3:org.apache.ibatis.executor.BaseExecutor

public abstract class BaseExecutor implements Executor {
    // ...
    // 一级缓存对象
    protected PerpetualCache localCache;
    // 存储过程输出参数缓存
    protected PerpetualCache localOutputParameterCache;
    // ...
    
    protected BaseExecutor(Configuration configuration, Transaction transaction) {
        // ...
        this.localCache = new PerpetualCache("LocalCache");
        this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
        // ...
    }
}

由 源码3 可知,一级缓存使用PerpetualCache来实现,BaseExecutor中维护了两个PerpetualCache属性,localCache用于缓存MyBatis查询结果,localOutputParameterCache用于缓存存储过程输出参数。 这两个属性均在BaseExecutor的构造方法中初始化,并指定其ID。

MyBatis通过CacheKey对象来描述缓存的Key值。如果两次查询操作的CacheKey对象相同,就认为这两次查询执行的是相同的SQL语句。 CacheKey对象通过BaseExecutor的createCacheKey()方法来创建。

java 复制代码
源码4:org.apache.ibatis.executor.BaseExecutor

@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
        throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    // Mapper的ID
    cacheKey.update(ms.getId());
    // 偏移量
    cacheKey.update(rowBounds.getOffset());
    // 查询条数
    cacheKey.update(rowBounds.getLimit());
    // SQL语句
    cacheKey.update(boundSql.getSql());
    // ......
    // SQL语句中的参数
    cacheKey.update(value);
    // ......
    // 配置文件中的<environment>标签的ID属性值
    if (configuration.getEnvironment() != null) {
        cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
}

由 源码4 可知,与CacheKey相关的因素包括:Mapper的ID、偏移量、查询条数、SQL语句、SQL语句中的参数、配置文件中的<environment>标签的ID属性值。

执行两次查询时,只有以上因素完全相同,才会认为这两次查询执行的是相同的SQL语句,才会直接从缓存中获取查询结果。

6.2.1 一级缓存在查询时的使用

解析来研究一下BaseExecutor的query()方法中是如何使用一级缓存的。

java 复制代码
源码5:org.apache.ibatis.executor.BaseExecutor

@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.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
        // 如果<select>标签的flushCache属性为true,则直接清除缓存
        // 默认为false
        clearLocalCache();
    }
    List<E> list;
    try {
        queryStack++;
        // 从一级缓存中根据CacheKey获取缓存结果
        list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
        if (list != null) {
            // 对存储过程输出参数的处理
            handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
        } else {
            // 没有获取到缓存结果,则从数据库查询
            list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
        }
    } finally {
        queryStack--;
    }
    if (queryStack == 0) {
        for (DeferredLoad deferredLoad : deferredLoads) {
            deferredLoad.load();
        }
        // issue #601
        deferredLoads.clear();
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            // 如果localCacheScope参数值为STATEMENT,缓存仅对当前执行的SQL语句有效,当语句执行完毕后,缓存就会被清除
            clearLocalCache();
        }
    }
    return list;
}

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

由 源码5 可知,在BaseExecutor的query()方法中,首先会判断<select>标签的flushCache属性值,如果该属性为true,说明任何SQL语句被调用都需要先清除缓存,因此直接调用clearLocalCache()方法清除缓存。该属性在<select>标签中的默认值为false。

接着根据CacheKey从一级缓存中查找是否有缓存对象。如果查找不到,则调用queryFromDatabase()方法从数据库查询数据,并将查询结果保存到一级缓存中;如果查找到了,则直接返回。

最后,该方法还会判断主配置文件中的localCacheScope参数的值是否为STATEMENT,如果是STATEMENT,缓存仅对当前执行的SQL语句有效,因此当语句执行完毕后,直接调用clearLocalCache()方法清除缓存。

6.2.2 一级缓存在更新时的清空

除了flushCache属性和localCacheScope属性可以控制一级缓存的清空,MyBatis会在执行任意更新语句 时清空缓存,即BaseExecutor的update()方法:

java 复制代码
源码6:org.apache.ibatis.executor.BaseExecutor

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

由 源码6 可知,MyBatis在调用doUpdate()方法执行更新语句之前,会调用clearLocalCache()方法清除缓存。

下面做个简单的测试。有以下单元测试代码,两次调用相同的selectAll()方法:

java 复制代码
@Test
public void testCache() throws IOException, NoSuchMethodException {
    Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    // 第一次调用selectAll
    userMapper.selectAll();
    // 第二次调用selectAll
    userMapper.selectAll();
}

借助Debug工具,可以发现第一次调用selectAll()方法是一级缓存中没有数据,程序会调用queryFromDatabase()方法从数据库查询数据,并存放到一级缓存中:

第二次调用selectAll()方法时,一级缓存中已经保存了第一次查询的数据,这次直接从缓存中就可以取到数据,而不需要再去数据库查询:

6.3 MyBatis二级缓存的实现原理

6.3.1 实现的二级缓存的Executor类型

MyBatis的官方文档 中说,默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。要启用全局的二级缓存,只需要在你的SQL映射文件中添加一行:<cache/>

官方文档意思是,默认情况下,一级缓存是打开的,二级缓存是关闭的。要开启二级缓存,需要在SQL映射文件中添加一行:<cache/>

java 复制代码
源码7:org.apache.ibatis.builder.xml.XMLConfigBuilder

private void settingsElement(Properties props) {
    // ......
    configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true));
    // ......
    configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty("defaultExecutorType", "SIMPLE")));
    // ......
}

由 源码7 可知,在解析MyBatis主配置文件时,cacheEnabled属性的默认值是true,即默认使用二级缓存(允许使用不代表开启);defaultExecutorType属性的默认值是SIMPLE,即默认创建的Executor类型是SimpleExecutor。

前面提到,二级缓存是在CachingExecutor类中完成的。因此,MyBatis在创建Executor时,会根据主配置文件中的cacheEnabled属性和defaultExecutorType属性来判断创建哪种Executor。

该创建工作在Configuration对象的工厂方法newExecutor()中完成:

java 复制代码
源码8:org.apache.ibatis.session.Configuration

// 默认创建SimpleExecutor
protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
// 默认开启二级缓存
protected boolean cacheEnabled = true;

public Executor newExecutor(Transaction transaction) {
    return newExecutor(transaction, defaultExecutorType);
}

// 指定Executor类型
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : 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);
    }
    // 如果cacheEnabled属性为true,则创建CachingExecutor
    if (cacheEnabled) {
        executor = new CachingExecutor(executor);
    }
    return (Executor) interceptorChain.pluginAll(executor);
}

由 源码8 可知,MyBatis默认允许使用二级缓存,其实现在CachingExecutor类中。

java 复制代码
源码9:org.apache.ibatis.executor.CachingExecutor

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

由 源码9 可知,CachingExecutor类中维护了一个TransactionalCacheManager实例,用于管理所有的二级缓存对象。

java 复制代码
源码10:org.apache.ibatis.cache.TransactionalCacheManager

public class TransactionalCacheManager {

    // 通过HashMap对象维护二级缓存对应的TransactionalCache实例
    private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
    
    // 清空缓存
    public void clear(Cache cache) {
        getTransactionalCache(cache).clear();
    }
    
    // 获取二级缓存对应的TransactionalCache对象
    // 根据根据缓存Key获取缓存对象
    public Object getObject(Cache cache, CacheKey key) {
        return getTransactionalCache(cache).getObject(key);
    }
    
    // 添加缓存
    public void putObject(Cache cache, CacheKey key, Object value) {
        getTransactionalCache(cache).putObject(key, value);
    }
    
    // 只有调用commit()方法后缓存对象才会真正添加到TransactionalCache中
    public void commit() {
        for (TransactionalCache txCache : transactionalCaches.values()) {
            txCache.commit();
        }
    }
    
    // 当调用rollback()方法时,写入操作将被回滚
    public void rollback() {
        for (TransactionalCache txCache : transactionalCaches.values()) {
            txCache.rollback();
        }
    }
    
    // 如果二级缓存对应的TransactionalCache获取不到,则创建一个新的
    private TransactionalCache getTransactionalCache(Cache cache) {
        return MapUtil.computeIfAbsent(transactionalCaches, cache, TransactionalCache::new);
    }

}

由 源码10 可知,TransactionalCacheManager类中组合了一个HashMap对象,用于维护二级缓存实例对应的TransactionalCache对象。getObject()putObject()方法均要先调用getTransactionalCache()方法获取到TransactionalCache对象,再对TransactionalCache对象进行操作。

java 复制代码
源码11:org.apache.ibatis.cache.decorators.TransactionalCache

public class TransactionalCache implements Cache {
    
    // 缓存对象,内部组合了一个HashMap实例
    private final Cache delegate;
    // 一个标志,为true时表示提交数据时要清空缓存对象,默认为false
    private boolean clearOnCommit;
    // 保存即将存入二级缓存的数据
    private final Map<Object, Object> entriesToAddOnCommit;
    // 保存从二级缓存中没有取出的数据时的缓存Key
    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 Object getObject(Object key) {
        // 从二级缓存Cache对象中获取缓存数据
        Object object = delegate.getObject(key);
        if (object == null) {
            // 从二级缓存中没有取出数据,则将这个缓存Key保存下来
            entriesMissedInCache.add(key);
        }
        if (clearOnCommit) {
            return null;
        }
        return object;
    }
    
    @Override
    public void putObject(Object key, Object object) {
        // 将要缓存的数据保存到HashMap集合(还没有真正加入到二级缓存中)
        entriesToAddOnCommit.put(key, object);
    }
    
    // 提交数据
    public void commit() {
        if (clearOnCommit) {
            delegate.clear();
        }
        // 刷新待处理的数据
        flushPendingEntries();
        // 清空两个容器的数据
        reset();
    }
    
    // 回滚数据
    public void rollback() {
        unlockMissedEntries();
        reset();
    }
    
    private void flushPendingEntries() {
        // 将entriesToAddOnCommit容器中的数据一一添加到Cache对象中
        for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
            delegate.putObject(entry.getKey(), entry.getValue());
        }
        // 遍历entriesMissedInCache容器中的缓存Key
        // 去重后将Value值置空,添加到Cache对象中
        for (Object entry : entriesMissedInCache) {
            if (!entriesToAddOnCommit.containsKey(entry)) {
                delegate.putObject(entry, null);
            }
        }
    }
    
    private void reset() {
        clearOnCommit = false;
        entriesToAddOnCommit.clear();
        entriesMissedInCache.clear();
    }
    
    private void unlockMissedEntries() {
        // 遍历entriesMissedInCache容器中的缓存Key
        // 逐一从Cache对象中移除
        for (Object entry : entriesMissedInCache) {
            try {
                delegate.removeObject(entry);
            } // catch ...
        }
    }
}

由 源码11 可知,TransactionalCache类对Cache类进行了增强,除了组合一个Cache对象以保存缓存对象,还分别组合了一个HashMap容器和一个HashSet容器,分别用于保存即将存入Cache对象的数据,以及保存从二级缓存中没有取出的数据时的缓存Key。

(1)如果要将数据存入二级缓存,则调用putObject()方法。 该方法将要缓存的数据保存到entriesToAddOnCommit容器。注意此时数据还没有真正保存到Cache对象中。

要想数据真正保存到Cache对象中,还需要调用commit()方法,该方法会将entriesToAddOnCommit容器中的数据一一添加到Cache对象中,还会以entriesMissedInCache容器中的缓存Key(去重后的)也添加到Cache对象中,只是它的Value值为null。

这样做的目的在于,即使某个缓存Key的查询结果为null,也要缓存,下次相同的缓存Key查询时,直接返回null即可。

(2)如果要将数据从二级缓存中取出来,则调用getObject()方法,该方法会根据缓存Key从Cache对象中取数据,如果没有取到数据,则将当前缓存Key保存到entriesMissedInCache容器中;取到数据则直接返回。

6.3.2 二级缓存在查询时使用

执行查询SQL语句时,会调用CachingExecutor类的query()方法中:

java 复制代码
源码12:org.apache.ibatis.executor.CachingExecutor

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler)
        throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // 构造缓存Key
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler,
                         CacheKey key, BoundSql boundSql) throws SQLException {
    // 获取MappedStatement对象中维护的二级缓存对象
    Cache cache = ms.getCache();
    if (cache != null) {
        // 判断是否需要刷新二级缓存
        flushCacheIfRequired(ms);
        // 主配置文件中的cacheEnabled属性为true时使用二级缓存
        if (ms.isUseCache() && resultHandler == null) {
            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;
        }
    }
    // 没有二级缓存时,直接从数据库中查询
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    if (cache != null && ms.isFlushCacheRequired()) {
        // 如果<select>标签的flushCache属性为true,则直接清除缓存。默认为false
        tcm.clear(cache);
    }
}

由 源码12 可知,CachingExecutor类的query()方法的逻辑如下:

(1)调用createCacheKey()方法创建缓存Key对象;

(2)调用MappedStatement对象的getCache()方法获取维护的二级缓存对象。如果有,则进入使用二级缓存的逻辑,如果没有则直接从数据库中查询。

(3)判断是否需要刷新二级缓存。如果<select>标签的flushCache属性为true(<select>标签中默认为false),则直接调用TransactionalCacheManager的clear()方法清除缓存。

(4)判断主配置文件中的cacheEnabled属性,为true时真正使用二级缓存。

(5)根据缓存Key从二级缓存中获取缓存数据,如果获取到了则直接返回,如果二级缓存中没有获取到数据,则从数据库中查询,再将数据库查询结果保存到二级缓存中。

6.3.3 二级缓存在更新时清空

和一级缓存一样,二级缓存也会在执行更新语句时被清空,即CachingExecutor类的update()方法:

java 复制代码
源码13:org.apache.ibatis.executor.CachingExecutor

@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    // 必要时清空二级缓存
    flushCacheIfRequired(ms);
    // 执行更新语句
    return delegate.update(ms, parameterObject);
}

由 源码13 可知,在执行更新SQL语句之前,会根据<select|insert|update|delete>标签的flushCache属性来判断是否需要清空二级缓存。

而<select>标签的flushCache属性值默认为false,<insert|update|delete>标签的flushCache属性值默认为true,因此在执行更新语句时会清空二级缓存。

...

下面做个简单的测试。沿用上面的单元测试代码,两次调用相同的selectAll()方法,不一样的是,查询后需要手动调用commit()方法(SELECT语句不会自动调用commit()方法)

java 复制代码
@Test
public void testCache() throws IOException, NoSuchMethodException {
    Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
    SqlSession sqlSession = sqlSessionFactory.openSession();
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    // 第一次调用selectAll并手动提交
    userMapper.selectAll();
    sqlSession.commit();
    // 第二次调用selectAll并手动提交
    userMapper.selectAll();
    sqlSession.commit();
}

另外,还需要在SQL映射文件中添加一行:<cache/>

借助Debug工具,可以发现第一次调用selectAll()方法时二级缓存中没有数据,程序会调用query()方法从数据库查询数据,并存放到二级缓存中:

第二次调用selectAll()方法时,二级缓存中已经保存了第一次查询的数据,这次直接从缓存中就可以取到数据,而不需要再去数据库查询:

注意,如果单元测试中没有sqlSession.commit();这一行代码,会发现数据不会保存到二级缓存中。

...

本节完,更多内容请查阅分类专栏:MyBatis3源码深度解析

相关推荐
高压锅_12206 分钟前
SQLAlchemy数据库连接密码特殊字符处理完全指南
数据库·mysql·django·sqlalchemy
Hello.Reader5 小时前
Redis 延迟监控深度指南
数据库·redis·缓存
ybq195133454315 小时前
Redis-主从复制-分布式系统
java·数据库·redis
好奇的菜鸟8 小时前
如何在IntelliJ IDEA中设置数据库连接全局共享
java·数据库·intellij-idea
tan180°8 小时前
MySQL表的操作(3)
linux·数据库·c++·vscode·后端·mysql
满昕欢喜8 小时前
SQL Server从入门到项目实践(超值版)读书笔记 20
数据库·sql·sqlserver
DuelCode9 小时前
Windows VMWare Centos Docker部署Springboot 应用实现文件上传返回文件http链接
java·spring boot·mysql·nginx·docker·centos·mybatis
优创学社29 小时前
基于springboot的社区生鲜团购系统
java·spring boot·后端
why技术9 小时前
Stack Overflow,轰然倒下!
前端·人工智能·后端
幽络源小助理9 小时前
SpringBoot基于Mysql的商业辅助决策系统设计与实现
java·vue.js·spring boot·后端·mysql·spring