一. 为什么要有一级缓存
每当我们使用Mybatis开启一次和数据库的会话, 就会创建一个SqlSession对象来表示这个会话。就在这一次会话中, 我们有可能反复执行完全相同的查询语句, 这些相同的查询语句在没有执行过更新的情况下返回的结果也是一致的。相信机智的你已经想到, 如果每次都去和数据库进行交互查询的话, 就会造成资源浪费。 所以, mybatis加入了一级缓存, 用来在一次会话中缓存查询结果。
总结下一级缓存的存在起到的作用: 在同一个会话里面,多次执行相同的sql语句(statementId, 参数, rowbounds完全相同),会直接从内存取到缓存的结果,不会再发送sql到数据库与数据库交互。但是不同的会话里面,即使执行的sql一模一样,也不能使用到一级缓存。
二. 一级缓存与会话的关系
一级缓存也叫本地缓存,MyBatis 的一级缓存是在会话层面(SqlSession)进行缓存的。默认开启,不需要任何的配置。
首先我们先思考一个问题,在MyBatis 执行的流程里面,涉及到这么多的对象,那么缓存Cache 应该放在哪个对象里面去维护?
先来进行一下推断, 我们已经知道一级缓存的作用范围是会话,那么这个对象肯定是在SqlSession 里面创建的,作为SqlSession 的一个属性存在。SqlSession本身是一个接口, 它的实现类DefaultSqlSession 里面只有两个属性---Configuration和Executor。Configuration 是全局的,与我们知道的一级缓存的作用范围不符, 所以缓存只可能放在Executor 里面维护---而事实也正是如此, SimpleExecutor/ReuseExecutor/BatchExecutor 的父类BaseExecutor 的构造函数中就持有了Cache。
那到底是不是这样的呢....关门, 放源码!
(1)创建会话的源码部分:
首先是调用DefauldSqlSessionFactory的openSession()方法, 即:开启会话
openSession()方法中调用了openSessionFromDataSource()方法,openSessionFromDataSource()方法中先是调用 configuration.newExecutor(tx, execType)创建了执行器(executor), 然后调用DefaultSqlSession的构造方法, 并传入了创建好的执行器(executor), 这样就创建出了DefaultSqlSession对象并让其持有了executor属性。
DefauldSqlSessionFactory:
//创建会话的方法
@Override
public SqlSession openSession(ExecutorType execType) {
return openSessionFromDataSource(execType, null, false);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 注意: 看这里!!创建Executor执行器
final Executor executor = configuration.newExecutor(tx, execType);
// 注意: 看这里!!创建DefaultSqlSession, executor作为DefaultSqlSession构造方法的一个参数传入
//DefaultSqlSession持有了executor
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
(2)创建执行部分源码
而创建执行器的时候, 会根据具体传入的执行器(executor)的类型, 来选择一个合适的执行器(executor)创建出来。但是不管最终选择哪个执行器, 他们都是BaseExecutor的子类 (缓存执行器除外, 涉及二级缓存相关, 这里暂且不提, 会专门写二级缓存的文章), 而我们的一级缓存, 正是BaseExecutor的一个属性, 而创建好的执行器作为BaseExecutor的子类也有着父类的属性。所以SqlSession对象持有了executor属性, 而executor持有了一级缓存。我们之前的一级缓存与会话的关系也得到了印证。
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) {
executor = new CachingExecutor(executor);
}
// 插件执行
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
看下BaseExecutor的属性, 它持有了PerpetualCache, 也就是一级缓存。
public abstract class BaseExecutor implements Executor:
protected PerpetualCache localCache;
既然PerpetualCache就是一级缓存了, 我们现在就来康康一级缓存到底是个啥吧, 没错, 最终这些东西都存在了一个HashMap里面。
public class PerpetualCache implements Cache {
private final String id;
//一级缓存最终存入容器
private Map
三. 一级缓存的生命周期
当会话结束时,SqlSession对象及其内部的Executor对象还有Cache对象也一并释放掉。
如果SqlSession调用了close()方法,会释放掉一级缓存Cache对象,一级缓存将不可用;
如果SqlSession调用了clearCache(),会清空Cache对象中的数据,但是该对象仍可使用;
SqlSession中执行了任何一个update操作(update()、delete()、insert()) ,都会清空Cache对象的数据,但是该对象可以继续使用;
四. 一级缓存的执行流程概要
缓存执行的大致思路与我们熟知的缓存思想一致。
(1)对于某个查询,根据statementId,params,rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果
(2)判断从Cache中根据特定的key值取的数据是否为空,即是否命中;
(3)如果命中,则直接将缓存结果返回;
(4)如果没命中, 去数据库中查询数据,得到查询结果;
将key和查询到的结果分别作为key,value对存储到Cache中;
将查询结果返回;
具体是怎么实现的呢, 这里又要放源码了:
(1)查询入口:
可以看见, 查询最终是调用了DefaultSqlSession持有的属性executor的query()方法。
DefaultSqlSession:
private final Configuration configuration;
private final Executor executor;
public List selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
//根据传入的statementId,获取MappedStatement对象
MappedStatement ms = configuration.getMappedStatement(statement);
//RowBounds是用来逻辑分页(按照条件将数据从数据库查询到内存中,在内存中进行分页)
// wrapCollection(parameter)是用来装饰集合或者数组参数
// 注意:看这里 !! 调用执行器的查询方法
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
executor的query()方法进行了一级缓存的逻辑, 会调用localCache.getObject(key)从缓存中获取数据, 如果获取不到, 又会调用queryFromDatabase()方法。见名知意, 这个方法就是用来来与数据库进行交互取数据。
在queryFromDatabase()方法中, 调用doQuery()来执行查询, 再把得到的结果调用localCache.putObject(key, list)放入一级缓存。
如果我们继续查看doQuery()方法, 就会发现这个方法是抽象的, 这里涉及到了一个常用的设计模式: 模板模式。真正的doQuery()方法的实现是在BaseExecutor的子类方法中去完成的, 完成从数据库中查询数据封装数据的部分, 暂且不提。
TIPS:模板模式(Template Pattern), 一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。
BaseExcecutor:
protected PerpetualCache localCache;
Override
public List 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()) {
clearLocalCache();//清除缓存
}
List list;
try {
queryStack++;
// 注意: 看这里!!从一级缓存中获取数据
list = resultHandler == null ? (List) 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) {
// issue #482
clearLocalCache();
}
}
return list;
}
private List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List 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;
}
// 注意: 看这里!! 这是一个抽象的方法, 等着子类去实现
protected abstract List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
throws SQLException;
五. 结构与总计(不涉及二级缓存)
小结: sqlSession 持有 BaseExecutor , BaseExecutor持有了一级缓存, 查询时调用BaseExecutor的query()方法, 并在query()方法中完成了一级缓存的功能。
缓存查到了就返回查询结果, 查询不到就调用queryFromDatabase()方法, 然后queryFromDatabase()方法中调用doQuery()方法从数据库中查询数据, 然后放入一级缓存, 其中doQuery()方法是抽象的 , 需要BaseExecutor的不同类型子类具体实现。
整体结构图如下:
怎么样, 现在对mybatis一级缓存是如何实现的是不是有了大概的了解~
这里除了编程知识分享,同时也是我成长脚印的记录, 期待与您一起学习和进步。如果您觉得这篇文章帮助到了您, 请帮忙点赞! 获取更多mybatis姿势, 也可以微信搜索: 程序媛swag。