为方便理解,本章涉及示例代码已上传至 gitee
==>获取示例代码请点击这里。。。
拉取示例代码时,请拉取所有分支,master 分支只是做了示例的初始化
MyBatis 的缓存一共有两种,一级缓存、二级缓存;一级缓存为框架的默认缓存,不可修改配置,二级缓存可以修改和配置,要理解 MyBatis 的缓存过程,首先需要对 MyBatis 的执行器有一定的了解,下图为 MyBatis 的各执行器之间的关系:
当初始化 MyBatis 配置的时候,会创建一个 CachingExecutor 类型的对象,其持有一个 Executor 类型的属性,所有的增删改查操作,先通过 CachingExecutor 进行一次处理,再交给 delegate 来执行;其中,CachingExecutor 中的处理即为二级缓存的处理,在处理完二级缓存后,delegate 会执行 BaseExecutor 类中的方法,BaseExecutor 类负责完成对一级缓存的查询或者清空。这里可能会有点乱,结合下图一起理解:
在 MyBatis 中,所有的增删改操作,最终都是通过 CachingExecutor 类中的 update 方法来执行的,在 CachingExecutor 中,query() 和 update() 方法统一先去操作一遍二级缓存,然后,在进入 BaseExecutor 中根据操作不同,对一级缓存执行不同的操作。
了解了 MyBatis 在什么时候使用一二级缓存后,接下来分别看看 MyBatis 的一二级缓存是如何实现的。
一级缓存
MyBatis 一级缓存对象的创建,是在初始化 MyBatis 配置时,会创建一个默认的 SimpleExecutor 对象,创建执行器源码如下:
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 {
// 默认会创建一个 SimpleExecutor 执行器
executor = new SimpleExecutor(this, transaction);
}
// cacheEnabled 默认为 true,为 true 则表示启用二级缓存,只是启用,如果要使用时,还需配置。
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
从该方法中可以看出,无论是否创建 CachingExecutor 执行器(二级缓存),一级缓存均会被使用到。
而一级缓存对象的创建,会在 SimpleExecutor 的构造方法中,其会调用父类的构造方法,由 BaseExecutor 来完成 一级缓存对象的创建。
protected BaseExecutor(Configuration configuration, Transaction transaction) {
...
this.localCache = new PerpetualCache("LocalCache");
...
}
使用的话,主要是在 BaseExecutor 类中的 update(…) 和 query(…) 这两个方法中会被使用到,分为清空和查询。
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
...
List<E> list;
try {
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--;
}
...
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;
}
// 清空一级缓存
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
再来看 PerpetualCache 类的具体代码:
public class PerpetualCache implements Cache {
private final String id;
// 所有的缓存键值对均会存储在 cache 中
private Map<Object, Object> cache = new HashMap<>();
public PerpetualCache(String id) {
this.id = id;
}
@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();
}
// other Methods...
}
从 PerpetualCache 的关键部分的源码中可以看出,一级缓存其实是以一个 HashMap 来进行键值的存储,整个存取过程完全是对 HashMap 在进行操作。
一级缓存作用域
一级缓存的默认作用域为 session 级别,在 MyBatis 初始化配置环境的时候,会调用 XMLConfigBuilder 的 settingsElement(…) 来进行配置:
XMLConfigBuilder.class
private void settingsElement(Properties props) {
// other confiuration
// 获取 props 的 localCacheScope 设置值,如果值为空,则将其设置为 SESSION
configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty("localCacheScope", "SESSION")));
// other configuration
}
// localCacheScope 共两个可选值:
public enum LocalCacheScope {
SESSION,STATEMENT
}
具体的作用于效果,我写了一个测试方法,如下:
@Test
public void testL1Cache(){
SqlSession sqlSession = sqlSessionFactory.openSession();
SysUserMapper mapper = sqlSession.getMapper(SysUserMapper.class);
System.err.println("执行第一遍查询~~~~");
mapper.selectById(1006);
System.err.println("执行第二遍查询~~~~");
mapper.selectById(1006);
sqlSession.close();
System.err.println("将原 sqlSesssion 对象关闭,重新开启一个新的 sqlSession1 ");
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SysUserMapper mapper1 = sqlSession1.getMapper(SysUserMapper.class);
System.err.println("执行第三遍查询~~~~");
mapper1.selectById(1006);
}
因为 MyBatis 的一级缓存默认作用域为 session 级别,所有,执行上面的测试方法,控制台应该只打印出两次查询的结果。实际打印结果如下:
执行第一遍查询~~~~
DEBUG [main] - ==> Preparing: select * from sys_user where id = ?
DEBUG [main] - ==> Parameters: 1006(Integer)
TRACE [main] - <== Columns: id, user_name, user_password, user_email, user_info, head_img, create_time
TRACE [main] - <== Row: 1006, 孙悟空, 123456, SWK@xiyouji.com, <<BLOB>>, <<BLOB>>, 2020-06-15 07:46:06
DEBUG [main] - <== Total: 1
执行第二遍查询~~~~
将原 sqlSesssion 对象关闭,重新开启一个新的 sqlSession1
执行第三遍查询~~~~
DEBUG [main] - ==> Preparing: select * from sys_user where id = ?
DEBUG [main] - ==> Parameters: 1006(Integer)
TRACE [main] - <== Columns: id, user_name, user_password, user_email, user_info, head_img, create_time
TRACE [main] - <== Row: 1006, 孙悟空, 123456, SWK@xiyouji.com, <<BLOB>>, <<BLOB>>, 2020-06-15 07:46:06
DEBUG [main] - <== Total: 1
通过上面的打印结果可以看出:执行第一遍查询时,一级缓存为空,所以,会去查询数据库,并将结果放到一级缓存中;当执行第二遍查询时,由于一级缓存已经有数据,故直接从缓存中拿到数据进行返回,不执行查询数据库操作;执行第三遍查询时,由于关闭原来的 sqlSession ,重新创建的这个 sqlSession 对象中没有缓存,所以,会再次查询数据库。
下一篇来说说二级缓存