黑马程序员技术交流社区
标题: Mybatis 缓存特性的使用及源码分析(2) [打印本页]
作者: hy2014051202 时间: 2017-7-27 19:22
标题: Mybatis 缓存特性的使用及源码分析(2)
一级缓存工作流程&源码分析
这一章节主要从一级缓存的工作流程和源码层面对一级缓存进行学习。
工作流程
根据一级缓存的工作流程,我们绘制出一级缓存执行的时序图,如下图所示。
主要步骤如下:
1. 对于某个Select Statement,根据该Statement生成key。
2. 判断在Local Cache中,该key是否用对应的数据存在。
3. 如果命中,则跳过查询数据库,继续往下走。
4. 如果没命中
4.1 去数据库中查询数据,得到查询结果;
4.2 将key和查询到的结果作为key和value,放入Local Cache中。
4.3. 将查询结果返回;
5. 判断缓存级别是否为STATEMENT级别,如果是的话,清空本地缓存。
源码分析
了解具体的工作流程后,我们队Mybatis查询相关的核心类和一级缓存的源码进行走读。这对于之后学习二级缓存时也有帮助。 SqlSession: 对外提供了用户和数据库之间交互需要的所有方法,隐藏了底层的细节。它的一个默认实现类是DefaultSqlSession。
Executor: SqlSession向用户提供操作数据库的方法,但和数据库操作有关的职责都会委托给Executor。
如下图所示,Executor有若干个实现类,为Executor赋予了不同的能力,大家可以根据类名,自行私下学习每个类的基本作用。
在一级缓存章节,我们主要学习BaseExecutor。
BaseExecutor: BaseExecutor是一个实现了Executor接口的抽象类,定义若干抽象方法,在执行的时候,把具体的操作委托给子类进行执行。
protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;
protected abstract List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException;
protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException;
protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException;
在一级缓存的介绍中,我们提到对Local Cache的查询和写入是在Executor内部完成的。在阅读BaseExecutor的代码后,我们也发现Local Cache就是它内部的一个成员变量,如下代码所示。
public abstract class BaseExecutor implements Executor {
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
protected PerpetualCache localCache;
Cache: Mybatis中的Cache接口,提供了和缓存相关的最基本的操作,有若干个实现类,使用装饰器模式互相组装,提供丰富的操控缓存的能力。
BaseExecutor成员变量之一的PerpetualCache,就是对Cache接口最基本的实现,其实现非常的简内部持有了hashmap,对一级缓存的操作其实就是对这个hashmap的操作。如下代码所示。
public class PerpetualCache implements Cache {
private String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();
在阅读相关核心类代码后,从源代码层面对一级缓存工作中涉及到的相关代码,出于篇幅的考虑,对源码做适当删减,读者朋友可以结合本文,后续进行更详细的学习。
为了执行和数据库的交互,首先会通过DefaultSqlSessionFactory开启一个SqlSession,在创建SqlSession的过程中,会通过Configuration类创建一个全新的Executor,作为DefaultSqlSession构造函数的参数,代码如下所示。
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
............
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
}
如果用户不进行制定的话,Configuration在创建Executor时,默认创建的类型就是SimpleExecutor,它是一个简单的执行类,只是单纯执行Sql。以下是具体用来创建的代码。
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);
}
// 尤其可以注意这里,如果二级缓存开关开启的话,是使用CahingExecutor装饰BaseExecutor的子类
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
在SqlSession创建完毕后,根据Statment的不同类型,会进入SqlSession的不同方法中,如果是Select语句的话,最后会执行到SqlSession的selectList,代码如下所示。
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
}
在上文的代码中,SqlSession把具体的查询职责委托给了Executor。如果只开启了一级缓存的话,首先会进入BaseExecutor的query方法。代码如下所示。
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
在上述代码中,会先根据传入的参数生成CacheKey,进入该方法查看CacheKey是如何生成的,代码如下所示。
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
//后面是update了sql中带的参数
cacheKey.update(value);
在上述的代码中,我们可以看到它将MappedStatement的Id、sql的offset、Sql的limit、Sql本身以及Sql中的参数传入了CacheKey这个类,最终生成了CacheKey。我们看一下这个类的结构。
private static final int DEFAULT_MULTIPLYER = 37;
private static final int DEFAULT_HASHCODE = 17;
private int multiplier;
private int hashcode;
private long checksum;
private int count;
private List<Object> updateList;
public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLYER;
this.count = 0;
this.updateList = new ArrayList<Object>();
}
首先是它的成员变量和构造函数,有一个初始的hachcode和乘数,同时维护了一个内部的updatelist。在CacheKey的update方法中,会进行一个hashcode和checksum的计算,同时把传入的参数添加进updatelist中。如下代码所示。
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);
}
我们是如何判断CacheKey相等的呢,在CacheKey的equals方法中给了我们答案,代码如下所示。
@Override
public boolean equals(Object object) {
.............
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;
}
除去hashcode,checksum和count的比较外,只要updatelist中的元素一一对应相等,那么就可以认为是CacheKey相等。只要两条Sql的下列五个值相同,即可以认为是相同的Sql。
Statement Id + Offset + Limmit + Sql + Params
BaseExecutor的query方法继续往下走,代码如下所示。
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);
}
如果查不到的话,就从数据库查,在queryFromDatabase中,会对localcache进行写入。
在query方法执行的最后,会判断一级缓存级别是否是STATEMENT级别,如果是的话,就清空缓存,这也就是STATEMENT级别的一级缓存无法共享localCache的原因。代码如下所示。
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache();
}
在源码分析的最后,我们确认一下,如果是insert/delete/update方法,缓存就会刷新的原因。
SqlSession的insert方法和delete方法,都会统一走update的流程,代码如下所示。
@Override
public int insert(String statement, Object parameter) {
return update(statement, parameter);
}
@Override
public int delete(String statement) {
return update(statement, null);
}
update方法也是委托给了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);
}
每次执行update前都会清空localCache。
至此,一级缓存的工作流程讲解以及源码分析完毕。
总结
Mybatis一级缓存的生命周期和SqlSession一致。
Mybatis的缓存是一个粗粒度的缓存,没有更新缓存和缓存过期的概念,同时只是使用了默认的hashmap,也没有做容量上的限定。
Mybatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,有操作数据库写的话,会引起脏数据,建议是把一级缓存的默认级别设定为Statement,即不使用一级缓存。
未完待续。。。。。。
欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) |
黑马程序员IT技术论坛 X3.2 |