目录
先上总结:
1,创建sqlSession阶段。创建Executor执行器,其中有两个参数,localCache和localOutputParameterCache,localCache是一级缓存,localOutputParameterCache用于缓存存储过程的OUT和INOUT参数。Executor执行器首先会被CachingExecutor装饰,然后又被插件列表用拦截器封装,创建动态代理,最后Executor成为了一个Plugin的动态代理。
2,执行sql前创建CacheKey阶段。CacheKey的生成调用的是CachingExecutor的createCacheKey方法,使用的是CacheKey的update方法,CacheKey包含hashcode,multiplier,count,updateList四个参数。会有6个参数参与update操作:MappedStatement.id,rowBounds的offset,rowBounds的limit,sql语句字符串,参数列表的非OUT类型参数值,environment对象的id。CacheKey的hashcode计算方式:count++,对象哈希值乘以count,新哈希值等于原哈希值乘以37再加上第2步的结果。
3,执行sql阶段的二级缓存。先判断是否有二级缓存,有就直接取二级缓存。二级缓存管理器除了保存有HashMap外,还有entriesToAddOnCommit(HashMap)负责收集未提交的查询结果,entriesMissedInCache(Set)负责收集收集二级缓存未覆盖的key。查询出的结果会先放在entriesToAddOnCommit中。
4,执行sql阶段的一级缓存。先判断是否有一级缓存,如果没有就直接取一级缓存。如果没有一级缓存就查数据库,查到结果保存到一级缓存中,如果是存储过程,还要处理localOutputParameterCache。另外,insert、update、delete操作会清空一级缓存。configuration的一级缓存的作用域LocalCacheScope配置为STATEMENT时也会每次查询后清空一级缓存。
5,sqlSession的提交阶段。对于一级缓存,如果没开事务则清空。对于二级缓存,如果事务需要提交,则会把entriesToAddOnCommit中的值保存至二级缓存,然后把entriesMissedInCache中的值保存到二级缓存且值为null,然后把这二者清空。
前言和总纲
1,MyBatis为我们定义的每个Mapper接口创建了一个MapperProxy动态代理。
2,当我们调用Mapper接口中的方法时,实际上调用的是MapperProxy动态代理的invoke()方法。
3,MapperProxy动态代理根据调用的select,update,insert,delete等语句,调用不同的子方法,以最简单的单行查询为例,调用的是:sqlSession.selectOne()方法
注意,此处的sqlSession指的是SqlSessionTemplate实例。
4,对于SqlSessionTemplate的selectOne()方法来说,他的代码是这样的:
public <T> T selectOne(String statement, Object parameter) {
return this.sqlSessionProxy.selectOne(statement, parameter);
}
这里的sqlSessionProxy由SqlSessionTemplate的内部类SqlSessionInterceptor提供:
private class SqlSessionInterceptor implements InvocationHandler {
private SqlSessionInterceptor() {
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
Object unwrapped;
try {
Object result = method.invoke(sqlSession, args);
if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
sqlSession.commit(true);
}
unwrapped = result;
} catch (Throwable var11) {
unwrapped = ExceptionUtil.unwrapThrowable(var11);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException)unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw (Throwable)unwrapped;
} finally {
if (sqlSession != null) {
SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
return unwrapped;
}
}
这段代码里面可以看到SqlSession的整个生命周期。
注意:这里的sqlSession是用来执行sql用的,对应MySQL的session会话,默认由DefaultSqlSession实现,和上面提到的SqlSessionTemplate不是一回事。前后用同名变量实在是让人有点眼晕。
下面关注以几个和缓存有关的重点,通过这几个点可以看到MyBatis是如何玩转缓存的:
1)创建SqlSession
SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
这段代码用来创建SqlSession,在这里面可以看到MyBatis缓存的代码结构。
2)执行sql
Object result = method.invoke(sqlSession, args);
这个方法负责执行sql,方法中会先查缓存,缓存不存在则查询数据库,然后会根据配置把查询结果放入缓存。同时要知道,MyBatis的缓存实际上以HashMap的形式存在,其中HashMap的key就是在正式开始查询前生成的。
3)SqlSession的提交
if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
sqlSession.commit(true);
}
SqlSession提交时,对缓存进行了一系列后续的处理。
对于以上三点,下面分别介绍。
一,创建SqlSession
创建SqlSession的逻辑从这里开始:
SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
这段代码来自SqlSessionTemplate的内部类SqlSessionInterceptor,看一下SqlSessionUtils的getSqlSession()方法:
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
Assert.notNull(sessionFactory, "No SqlSessionFactory specified");
Assert.notNull(executorType, "No ExecutorType specified");
SqlSessionHolder holder = (SqlSessionHolder)TransactionSynchronizationManager.getResource(sessionFactory);
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
} else {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Creating a new SqlSession");
}
session = sessionFactory.openSession(executorType);
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
}
逻辑很简单,先通过Spring的事务管理器判断当前session是否已经处于在事务中,如果是则直接返回session,如果没有则创建一个session,调用的是这行代码:
session = sessionFactory.openSession(executorType);
这里的sessionFactory是DefaultSqlSessionFactory对象,他的openSession()方法是这样的:
@Override
public SqlSession openSession(ExecutorType execType) {
return openSessionFromDataSource(execType, null, false);
}
继续,openSessionFromDataSource()方法:
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);
final Executor executor = configuration.newExecutor(tx, execType);
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();
}
}
在这段代码中出现了一个重要的对象,Executor执行器,sql将由此对象执行,executor执行器由以下方法创建:
final Executor executor = configuration.newExecutor(tx, execType);
这里的configuration对象来自MyBatis的Configuration类,他的newExecutor()方法是这样的:
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;
}
可见,根据executorType种类的不同,executor有几种不同的实现类,默认情况下由SimpleExecutor类来实现。
需要注意的是,SimpleExecutor有个父类:BaseExecutor,SimpleExecutor的构造方法中调用了BaseExecutor的构造方法:
protected BaseExecutor(Configuration configuration, Transaction transaction) {
this.transaction = transaction;
this.deferredLoads = new ConcurrentLinkedQueue<DeferredLoad>();
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
this.closed = false;
this.configuration = configuration;
this.wrapper = this;
}
注意到,这其中除了参数传入之外,还初始化了两个属性:
this.localCache = new PerpetualCache("LocalCache");
this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
都是PerpetualCache类的对象,这个类是MyBatis专门处理一级缓存用的,但是BaseExecutor设置了两个PerpetualCache对象,其中localCache就是通常意义上的一级缓存,而localOutputParameterCache只有存储过程会用到,他缓存了存储过程的OUT和INOUT类型参数的输出结果。
PerpetualCache类中维护了一个HashMap:
private Map<Object, Object> cache = new HashMap<Object, Object>();
专门用来保存一级缓存的信息。
生成了executor的实现类后,判断了一下cacheEnabled参数,cacheEnabled是二级缓存用的参数,用户可以在配置文件中使用cacheEnabled=true,或者使用注解@CacheNameSpace来进行配置。此参数默认就是true。
注意:cacheEnabled参数为true不代表一定会使用二级缓存,二级缓存的使用需要几个配置同时生效。
继续代码逻辑,当cacheEnabled参数为true时,executor执行器被替换成了一个CachingExecutor对象,原来的执行器对象作为参数传入,这是一种装饰者模式。CachingExecutor的构造是这样的:
public CachingExecutor(Executor delegate) {
this.delegate = delegate;
delegate.setExecutorWrapper(this);
}
可见,原来的SimpleExecutor对象成为了CachingExecutor的其中一个属性delegate,这种装饰者模式扩展了原有对象的功能。
再后面的代码,executor执行器又变了,被interceptorChain添加了插件列表,interceptorChain使用了责任链模式,保存了拦截器列表,实际上就是插件列表,每个插件其实都是一个拦截器。interceptorChain的pluginAll()方法是这样的:
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
可以看到,如果插件列表中有插件,那么CachingExecutor就会被拦截器处理,调用拦截器的plugin()方法,CachingExecutor对象会变成一个动态代理Proxy。如果插件列表中什么插件都没有,返回的CachingExecutor就会是之前的CachingExecutor对象。
下面以MyBatis的分页插件PageHelper为例,看这个插件是如何影响CachingExecutor的。
PageHelper中的拦截器是PageInterceptor类,他的plugin()方法是这样的:
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
其中target参数就是传入的CachingExecutor(也有可能是被之前拦截器处理过的动态代理),this参数就是这个拦截器本身,下面是Plugin.wrap()的代码:
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
逻辑大概就是:拿到class对象,接口列表,然后创建动态代理。
注意newProxyInstance()方法的第三个参数:new Plugin(target, interceptor, signatureMap),这个Plugin对象将成为动态代理的InvocationHandler,于是这个动态代理就成了一个Plugin代理。
Plugin的这个构造其实就是传入了这几个参数,没有其他逻辑了:
private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}
我们看到,Plugin的target就是原始的CachingExecutor对象,这个后面会用到。
得到了Plugin代理后,这个代理将被赋值给executor执行器。于是executor执行器的最终形态可能是一个CachingExecutor对象,或是一个Plugin代理。
执行器创建完成,我们回到openSessionFromDataSource()方法:
final Executor executor = configuration.newExecutor(tx, execType);
这段代码执行完成,下面是:
return new DefaultSqlSession(configuration, executor, autoCommit);
根据刚刚创建的执行器等参数构造了一个DefaultSqlSession,构造方法里也没别的逻辑,都是参数传入。
至此,创建SqlSession部分结束。
二,创建CacheKey
重新贴一下SqlSessionInterceptor的invoke()方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
Object unwrapped;
try {
Object result = method.invoke(sqlSession, args);
if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
sqlSession.commit(true);
}
unwrapped = result;
} catch (Throwable var11) {
unwrapped = ExceptionUtil.unwrapThrowable(var11);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException)unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw (Throwable)unwrapped;
} finally {
if (sqlSession != null) {
SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
return unwrapped;
}
在上一节中创建好了sqlSession,这一节将开始调用:
Object result = method.invoke(sqlSession, args);
来执行sql,在正式执行sql之前,会先创建缓存用的key。
此处method的invoke方法经过一系列反射的调用,代码会来到DefaultSqlSession的selectList()方法:
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
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();
}
}
这里的DefaultSqlSession就是上一节创建好的sqlSession,这段代码的重点是下面这个query()方法:
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
从上一节中我们知道,这里的executor对象可能是CachingExecutor对象或者Plugin的动态代理,如果是CachingExecutor对象,此处将直接调用CachingExecutor的query()方法,如果是动态代理,则会调用Plugin的invoke()方法,方法最终还会调用CachingExecutor的query()方法。
本文中我们只关注代码逻辑中和缓存相关的部分,上面说的两种方式都有相同的一段逻辑,就是调用CachingExecutor的createCacheKey()方法获得key,然后调用query()方法查询。
下面分别看一下这两种情况。
第一种情况,没有拦截器,executor就是CachingExecutor对象。直接调用query()方法:
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
可以看到,先是调用了createCacheKey()方法拿到key,然后调用query()方法查询。
第二种情况,有拦截器,executor是Plugin的动态代理,Plugin的invoke()方法是这样的:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
其中的method就是query方法,从signatureMap中能拿到这个方法,所以代码会返回:
return interceptor.intercept(new Invocation(target, method, args));
这里的interceptor就是Plugin里面的PageInterceptor,分页插件的拦截器,其intercept()方法是这样的:
public Object intercept(Invocation invocation) throws Throwable {
Object var16;
try {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement)args[0];
Object parameter = args[1];
RowBounds rowBounds = (RowBounds)args[2];
ResultHandler resultHandler = (ResultHandler)args[3];
Executor executor = (Executor)invocation.getTarget();
CacheKey cacheKey;
BoundSql boundSql;
if (args.length == 4) {
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
} else {
cacheKey = (CacheKey)args[4];
boundSql = (BoundSql)args[5];
}
this.checkDialectExists();
if (this.dialect instanceof Chain) {
boundSql = ((Chain)this.dialect).doBoundSql(Type.ORIGINAL, boundSql, cacheKey);
}
List resultList;
if (!this.dialect.skip(ms, parameter, rowBounds)) {
if (this.dialect.beforeCount(ms, parameter, rowBounds)) {
Long count = this.count(executor, ms, parameter, rowBounds, (ResultHandler)null, boundSql);
if (!this.dialect.afterCount(count, parameter, rowBounds)) {
Object var12 = this.dialect.afterPage(new ArrayList(), parameter, rowBounds);
return var12;
}
}
resultList = ExecutorUtil.pageQuery(this.dialect, executor, ms, parameter, rowBounds, resultHandler, boundSql, cacheKey);
} else {
resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}
var16 = this.dialect.afterPage(resultList, parameter, rowBounds);
} finally {
if (this.dialect != null) {
this.dialect.afterAll();
}
}
return var16;
}
这其中提到了invocation的args数组,这个args数组是什么?再回顾一下这个动态代理执行的那行代码,那是DefaultSqlSession的selectList方法,注意executor.query方法执行时的参数列表:
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
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();
}
}
args数组中的值就是ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER,四个参数组成的数组。
回到PageInterceptor的intercept()方法,注意到这段:
if (args.length == 4) {
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
}
这个数组长度为4的场景就是符合当前场景的,在逻辑中构造了cacheKey,用的是executor的createCacheKey()方法,此处的executor是
Executor executor = (Executor)invocation.getTarget();
得到的,也就是Plugin构造时传入的CachingExecutor对象,于是代码又来到了CachingExecutor的createCacheKey()方法。
这样,不论excutor执行器是CachingExecutor对象还是Plugin动态代理,方法都调用了CachingExecutor的createCacheKey方法,这个方法代码是这样的:
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
return delegate.createCacheKey(ms, parameterObject, rowBounds, boundSql);
}
这里的delegate就是生成CachingExecutor时用的SimpleExecutor,他的createCacheKey方法在其父类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();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
for (ParameterMapping parameterMapping : parameterMappings) {
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
cacheKey.update(value);
}
}
if (configuration.getEnvironment() != null) {
// issue #176
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
这个方法用于产生缓存用key,可以看到,方法创建了一个CacheKey对象,然后对cacheKey用几个参数进行了多次update操作,经过几次update后,得到的cacheKey对象将来会作为缓存的key。
下面看一下这个CacheKey和他的update()方法,CacheKey的构造方法是这样的:
public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLYER;
this.count = 0;
this.updateList = new ArrayList<Object>();
}
可以看到CacheKey包含了四个属性:
- hashcode,CacheKey的哈希值,CacheKey在update操作时会修改此值,是判定CacheKey是否相同的重要依据。
- multiplier,乘数,等于DEFAULT_MULTIPLYER变量,数值37,不会改变。
- count,经历的update次数,应该是当做计数器用的。
- updateList,每次update操作用到的对象列表。
另外CacheKey中还有个属性checksum,记录了每个参与update的对象的哈希值之和,也是校验CacheKey是否相同的依据之一。
然后就是CacheKey的update()方法:
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);
}
方法逻辑中,除了count加1,checksum+=对象哈希值,updateList添加对象之外,就是计算新的hashcode了,步骤分为以下几步:
- 计算对象哈希值。
- 对象哈希值乘以count。
- 新哈希值等于原哈希值乘以37再加上第2步的结果。
关于第2步乘以count的必要性,目测是为了使计算流程有序,相同的哈希值交换位置后能得到不同的结果,这一点后面会说。
以上便是每次update对hashcode的影响方式,根据createCacheKey()方法的代码,有以下对象参与了update:
- MappedStatement的id。这个字符串是调用的接口名加上方法名,比如com.test.TestMapper.getOrder。
- rowBounds的offset,分页配置的第一个参数,不写则为0。
- rowBounds的limit,分页配置的第二个参数,不写则为2147483647。
- sql语句字符串。如果是预编译模式那么这里的字符串就是预编译的sql语句,带问号的那种。
- 参数列表的非OUT类型参数。也就是IN和INOUT类型参数。
- configuration中的environment对象的id。
注意到处理参数列表的参数时,有这么一段代码:
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
metaObject是根据参数列表创建的一个工具类,保留了当前sql所有可接收的参数Map,其中key是参数名,value是给sql传的参数值。
然后调用getValue()方法,根据参数名获得传入的参数值。注意是参数值,和参数名没关系。
从这个逻辑中我们能得到两点结论:
1,只有sql需要的参数才会参与update操作,参数列表中传入的其他无关参数不会影响hashcode。
2,传入的参数值会参与update操作,和参数名无关。
查尔斯王子思考了一下,于是问题来了:
参数名不影响hashcode结果,如果有两次查询的查询条件如下:
第一次查询,字段A="abc" and 字段B="def",
第二次查询,字段A="def" and 字段B="abc",
那么参与update操作的对象都是"abc"和"def",最后会不会算出相同的hashcode,从而使第二次查询直接使用第一次查询的缓存?
目测这个问题就是计算hashcode的步骤中,对象hashcode要先乘以count的原因,只要保证两个查询的计算都先处理字段A再处理字段B(或反之),那么两个查询即使用的参数值相同,最终也能得到不同的hashcode。
假设这两个字段的hashcode分别要乘的count是5和6(因为MappedStatement.id要乘1,offset要乘2,limit要乘3,sql语句要乘4),那么两次查询时hashcode就会是这么算(只列出计算入参的部分):
第一次查询:
hashcode=hashcode*37+"abc".hashcode*5;
hashcode=hashcode*37+"def".hashcode*6;
第二次查询:
hashcode=hashcode*37+"def".hashcode*5;
hashcode=hashcode*37+"abc".hashcode*6;
于是两次查询得到不同的hashcode,第二次查询不会用第一次的缓存。
查尔斯王子又思考了一下,为什么不让字段名+字段值一起参与update呢?比如给"A=abc"这样,不就不用考虑顺序了么?
另外,我们看这个工具类的生成代码:
MetaObject metaObject = configuration.newMetaObject(parameterObject);
这段代码是写在参数列表的循环中的,但是这个对象本身和某个具体参数没关系,那为什么这行代码不写在循环外面,而是在循环内部一遍一遍的创建呢?只是为了快速给GC回收么?
在createCacheKey()方法的最后,cacheKey还用configuration中的environment对象的id进行了一次update操作,一般情况下这个environment对象的id就是"SqlSessionFactoryBean"。
至此,hashcode计算完成,cacheKey对象创建完成。
三,执行sql
cacheKey对象创建完成后,下面准备开始执行sql。不论是否使用了PageHelper插件,代码最终会执行到CachingExecutor的query()方法:
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
Cache cache = ms.getCache();
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
方法最开始的:
Cache cache = ms.getCache();
先获得cache对象,cache不为空说明配置了二级缓存,后面的逻辑就是,如果有二级缓存,则考虑从二级缓存中取值,否则就查数据库。
于是我们看到:MyBatis实际上是先访问二级缓存再访问一级缓存的。
下面看看从二级缓存中获取数据的逻辑,最开始调用了一个刷缓存的方法:
flushCacheIfRequired(ms);
方法的内容是这样的:
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
返回值取决于MappedStatement的isFlushCacheRequired()方法,这个方法的返回值来自MyBatis的xml配置文件,在方法节点中可以配置flushCache="true"参数,这个参数在<select>节点中默认是false,即不清空二级缓存,在<insert>、<update>、<delete>节点中默认是true,即数据更新操作会清空二级缓存。
方法中的tcm对象是CachingExecutor中的TransactionalCacheManager属性,这是CachingExecutor的二级缓存管理器。
继续看二级缓存的逻辑,在后面的代码中可以看到,并不是存在二级缓存就一定会用的,还有其他的条件,比如ms.isUseCache()==true。MappedStatement的isUseCache()方法的返回值来自MyBatis的xml配置文件,在<select>节点中可以配置useCache="true",不过这个参数默认就是true。
继续,如果<select>节点中配置了useCache="true",可以准备使用二级缓存了,不过在此之前还有一层判断:
ensureNoOutParams(ms, boundSql);
这是一个参数类别的判断:
private void ensureNoOutParams(MappedStatement ms, BoundSql boundSql) {
if (ms.getStatementType() == StatementType.CALLABLE) {
for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
if (parameterMapping.getMode() != ParameterMode.IN) {
throw new ExecutorException("Caching stored procedures with OUT params is not supported. Please configure useCache=false in " + ms.getId() + " statement.");
}
}
}
}
意思是开通了二级缓存的CALLABLE类型的sql中,参数类别必须是IN类型,OUT和INOUT不行,否则会抛异常。异常中说,缓存的存储过程不能支持OUT类型的参数,请把useCache配成false。
也就是说,只要参数中有IN类型就直接不让用二级缓存。
再往后就是从二级缓存中拿结果了,也就是这段:
List<E> list = (List<E>) tcm.getObject(cache, key);
tcm是TransactionalCacheManager的对象,这是MyBatis的CachingExecutor的二级缓存管理器,缓存保存在其中的一个HashMap属性里:
private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
这个Map的key就是之前生成的CachingKey,而他的value由TransactionalCache实现,这是某个具体的二级缓存对象。
tcm.getObject()方法体现了一些二级缓存的设计思路:
public Object getObject(Cache cache, CacheKey key) {
return getTransactionalCache(cache).getObject(key);
}
getTransactionalCache()方法就是获得TransactionalCache对象,然后调用他的getObject()方法:
@Override
public Object getObject(Object key) {
// issue #116
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
// issue #146
if (clearOnCommit) {
return null;
} else {
return object;
}
}
注意到其中有一个entriesMissedInCache对象,是个Set类型,存储了二级缓存之前没能覆盖到的缓存key(注意,只是key),这个key会在TransactionalCache的commit()方法时保存到二级缓存中,但是保存的value是个null,作用不太清楚,有网友指出这是为了防止缓存穿透。
如果没有拿到缓存信息,则需要调用:
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
进行查询,这个查询和没有使用二级缓存时的查询是一样的。
然后调用tcm.putObject()方法,注意,tcm.putObject()方法并不会把查询结果直接放入二级缓存,而是放入entriesToAddOnCommit这个Map,看看这个putObject()方法:
public void putObject(Cache cache, CacheKey key, Object value) {
getTransactionalCache(cache).putObject(key, value);
}
getTransactionalCache()方法获得的是一个具体的二级缓存对象,由TransactionalCache类实现,然后调用了他的putObject()方法:
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
可见,查询结果保存在了entriesToAddOnCommit对象中,entriesToAddOnCommit是TransactionalCache中维护的一个HashMap,相当于二级缓存的缓存,这个HashMap中的内容只有在调用TransactionalCache的commit()方法时才会正式放入二级缓存。这种事务提交之后才写入二级缓存的操作能避免脏读。
二级缓存的使用到此结束。
然后就是没有开通二级缓存的情况,CachingExecutor的query方法调用的也是:
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
和二级缓存一样的方法,这里的delegate是CachingExecutor封装的SimpleExecutor对象,他的query()方法在父类BaseExecutor中:
@SuppressWarnings("unchecked")
@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--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
重点关注一下try{}代码块中的内容,queryStack是计数器,用来给嵌套查询用的,queryStack=0的时候才算是最上层查询结束。
然后是从一级缓存中取查询结果,也就是这一段:
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
可见,一级缓存是从BaseExecutor的localCache对象中获取的,key就是之前生成的CacheKey对象。
继续看代码,如果一级缓存中保存了缓存信息,下面调用了这样一个方法:
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
这个方法是专门给CALLABLE类型sql用的,代码如下;
private void handleLocallyCachedOutputParameters(MappedStatement ms, CacheKey key, Object parameter, BoundSql boundSql) {
if (ms.getStatementType() == StatementType.CALLABLE) {
final Object cachedParameter = localOutputParameterCache.getObject(key);
if (cachedParameter != null && parameter != null) {
final MetaObject metaCachedParameter = configuration.newMetaObject(cachedParameter);
final MetaObject metaParameter = configuration.newMetaObject(parameter);
for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
if (parameterMapping.getMode() != ParameterMode.IN) {
final String parameterName = parameterMapping.getProperty();
final Object cachedValue = metaCachedParameter.getValue(parameterName);
metaParameter.setValue(parameterName, cachedValue);
}
}
}
}
}
从方法名字就能看出来,这个方法是处理存储过程的输出参数(OUT和INOUT)用的。
方法的大概逻辑就是,轮询了sql中的参数列表,筛选出其中不是IN类型的参数(也就是OUT和INOUT类型),缓存了这些参数被存储过程影响后的结果。
以上便是一级缓存中有值的情况,下面看看一级缓存中没值的场景,也就是:
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
这段代码,从数据库中获取list,queryFromDatabase()方法代码如下:
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;
}
我们看到,一开始调用:
localCache.putObject(key, EXECUTION_PLACEHOLDER);
这个方法,在一级缓存中添加了对应key的一个占位符。没看明白这么做的目的。
后面就是调用doQuery()方法查询数据库,这个方法里没有和缓存相关的逻辑,就是在查数据库。
在finally{}代码块中,又把刚刚的占位符给删了……
接着又重新往一级缓存localCache中添加了正确的查询结果。
然后不能忘了CALLABLE要用的localOutputParameterCache缓存,不过这里缓存的不是查询结果,而是查询的参数,因为OUT和INOUT类型的参数值会被存储过程影响。
做完这一切,数据的查询就完成了,我们还可以看看query()方法中的其他逻辑,比如代码中有两个地方调用了clearLocalCache()这个方法,这个方法用于清空一级缓存,方法代码是:
@Override
public void clearLocalCache() {
if (!closed) {
localCache.clear();
localOutputParameterCache.clear();
}
}
很简单,把BaseExecutor中的两个PerpetualCache缓存全部清空。
第一个清空一级缓存的地方是:
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
也就是说,MappedStatement的isFlushCacheRequired()方法返回true时清空一级缓存,二级缓存的逻辑里面也是用这个参数来清空的。
这个方法的返回值来自MyBatis的xml配置文件,在方法节点中可以配置flushCache="true"参数,这个参数在<select>节点中默认是false,即不清空二级缓存,在<insert>、<update>、<delete>节点中默认是true。
第二个清空缓存的地方就是:
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
configuration的LocalCacheScope参数是一级缓存的作用域,作用域是LocalCacheScope.STATEMENT时表示每次查询都清空一级缓存,与之相对的是LocalCacheScope.SESSION。
sql的执行过程就到此为止。
四,sqlSession的提交
再一次回到SqlSessionInterceptor的invoke()方法:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = SqlSessionUtils.getSqlSession(SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
Object unwrapped;
try {
Object result = method.invoke(sqlSession, args);
if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
sqlSession.commit(true);
}
unwrapped = result;
} catch (Throwable var11) {
unwrapped = ExceptionUtil.unwrapThrowable(var11);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException)unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw (Throwable)unwrapped;
} finally {
if (sqlSession != null) {
SqlSessionUtils.closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
return unwrapped;
}
在上一节中已经调用method.invoke()方法查到了结果,这一节将调用session的commit()方法,也就是这一段:
if (!SqlSessionUtils.isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
sqlSession.commit(true);
}
if判断中的条件是当前是否开启了MyBatis事务,如果没开启事务则会调用sqlSession的commit()方法,参数传的是true。
对于commit()方法来说如果参数是true则会提交或回滚事务,但是这里的调用和事务没什么关系,貌似只是为了清空一级缓存。
此处sqlSession对象由DefaultSqlSession类实现,他的commit()方法代码如下:
@Override
public void commit(boolean force) {
try {
executor.commit(isCommitOrRollbackRequired(force));
dirty = false;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
其中调用了executor的commit()方法,传入的参数代表是否需要提交事务。
dirty参数貌似是判断是否是脏数据用的,commit()之后就不是脏数据了。
如果使用了插件,这个executor就是一个Plugin的动态代理,所以这里的executor.commit()会调用Plugin的invoke方法:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
其中的signatureMap中是没有commit()这个方法的,所以会调用最后的method.invoke()方法,参数中的target就是生成Plugin代理时定义的CachingExecutor对象。
如果没用插件,executor就是CachingExecutor对象。
CachingExecutor的commit()方法是这样的:
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();
}
分为两部分,一部分是delegate的commit(),用于处理一级缓存,另一部分是tcm的commit(),用于处理二级缓存。
delegate对象就是生成CachingExecutor时传入的SimpleExecutor对象,他的commit()方法在父类BaseExecutor中:
@Override
public void commit(boolean required) throws SQLException {
if (closed) {
throw new ExecutorException("Cannot commit, transaction is already closed");
}
clearLocalCache();
flushStatements();
if (required) {
transaction.commit();
}
}
其中的clearLocalCache()上面看过了,清空一级缓存。
而flushStatements()这个方法没有做什么,这个方法返回的是一个空的List。
required如果是true则会提交事务,但是这里的调用和事务没什么关系。
于是我们发现:只要没开事务,一级缓存基本就用不上,每次查询之后都会给清除掉。
然后是tcm的commit()方法,tcm是TransactionalCacheManager的对象,是二级缓存管理器,他的commit()方法是这样的:
public void commit() {
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
其中调用的是TransactionalCache的commit()方法:
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
这个方法涉及二级缓存的一点设计机制,TransactionalCache中有clearOnCommit变量,如果在查询时clearOnCommit被设为true,那么调用commit()方法时将会清除二级缓存。
TransactionalCache中有entriesToAddOnCommit属性,是一个Map,相当于二级缓存的缓存,如果里面有值,那么调用commit()方法时将会把entriesToAddOnCommit的值放入二级缓存,也就是flushPendingEntries()方法的逻辑:
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
for (Object entry : entriesMissedInCache) {
if (!entriesToAddOnCommit.containsKey(entry)) {
delegate.putObject(entry, null);
}
}
}
这个方法还把之前保存的entriesMissedInCache这个Set存入了二级缓存,存的值是null。可能有防止缓存穿透的效果。
然后调用reset方法:
private void reset() {
clearOnCommit = false;
entriesToAddOnCommit.clear();
entriesMissedInCache.clear();
}
初始化clearOnCommit和entriesToAddOnCommit等参数。
于是commit()方法结束。
从上面的逻辑可以看到,Session提交时,一级缓存会被清除(无事务),而二级缓存会从entriesToAddOnCommit写入。
本文完