mybatis的缓存机制源码分析之一级缓存解析
Posted 犀牛饲养员
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了mybatis的缓存机制源码分析之一级缓存解析相关的知识,希望对你有一定的参考价值。
引言
本篇源码解析基于mybatis 3.5.8版本。
MyBatis 中的缓存指的是 MyBatis 在执行一次SQL查询时,在满足一定的条件下,会把这个sql和对应的查询结果缓存起来。当再次执行相同SQL语句的时候,就会直接从缓存中进行提取,而不是请求到数据库。当然如果中间有更新操作,缓存会失效。
MyBatis中的缓存分为一级缓存和二级缓存,一级缓存又被称为 SqlSession 级别的缓存,二级缓存又被称为表级缓存。通俗的说,一级缓存是本次会话有效,二级缓存可以跨越多个会话共享缓存。
当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。
一级缓存开启的情况下,查询的时序图如下:
二级缓存也是类似的机制。
正文
根据上面的调用时序图我们可以看到mybatis是通过SqlSession
统一对外的,SqlSession是接口,有个默认实现类:DefaultSqlSession
。来看下这个类。
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
//每个SqlSession中持有了Executor
private final Executor executor;
private final boolean autoCommit;
private boolean dirty;
private List<Cursor<?>> cursorList;
...
DefaultSqlSession
持有Executor
的引用,事实上对sqlSession的操作都是委托给这个Executor进行的,比如select方法,最终会调用selectList方法:
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
Executor
是个接口,有个默认的抽象类实现BaseExecutor
,这个抽象类持有一个名为``PerpetualCache`的引用,这是个本地缓存的实现类,缓存就是通过这个类来管理的。
public abstract class BaseExecutor implements Executor {
...
//本地缓存实现类
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration;
...
到这里,我们总结下提到的这几个类的关系:
前面提到BaseExecutor
是个抽象类,定义若干抽象方法,方法如下:
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;
在执行的时候,把具体的操作委托给对应的实现类进行执行。有几个实现类,为Executor赋予了不同的能力,如下图所示:
BatchExecutor专门用于执行批量sql操作。而SimpleExecutor用于简单sql场景。至于选择哪个,是配置决定的:
<setting name="defaultExecutorType" value="SIMPLE"/>
既然DefaultSqlSession
持有Executor
的引用,它是什么时候初始化的呢?答案是在org.apache.ibatis.session.defaults.DefaultSqlSessionFactory#openSession
调用的时候,如下:
private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
try {
....
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
继续跟下去,org.apache.ibatis.session.Configuration#newExecutor
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
//默认的executor类型是SIMPLE
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;
}
大部分时候,我们使用的是SimpleExecutor
,注意二级缓存如果开关打开使用的是CachingExecutor
,我们下篇文章再来分析二级缓存。 插件化处理那里,后面也会有专门的文章进行解析。
来继续看看PerpetualCache
,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()) {
clearLocalCache();
}
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--;
}
...
代码逻辑比较清晰,先查缓冲,没有命中就查数据库。PerpetualCache其实是持有一个map进行本地缓存。然后查询db后再更新cache:
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;
}
public class PerpetualCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap<>();
...
Cache
接口有几个实现类,结构如下:
这些不同的实现类彼此通过装饰器模式互相装饰,实现功能的互补。比如说,
public class LruCache implements Cache {
private final Cache delegate;
private Map<Object, Object> keyMap;
private Object eldestKey;
...
初始化的时候可以传入一个其它cache实现类的,复用这个类的功能。
LruCache cache = new LruCache(new PerpetualCache("default"));
cache.setSize(5);
for (int i = 0; i < 5; i++) {
cache.putObject(i, i);
}
...
具体的每个实现类不是本文的重点,这里不表。
一级缓存就写到这里吧。
参考
- https://tech.meituan.com/2018/01/19/mybatis-cache.html
以上是关于mybatis的缓存机制源码分析之一级缓存解析的主要内容,如果未能解决你的问题,请参考以下文章
Mybaits 源码解析 ----- 全网最详细,没有之一:一级缓存和二级缓存源码分析