在上一篇文章中我们主要对mybatis拦截器进行了研究。通过分析我们主要学习到mybatis这种插件的设计理念的优点。除此之外还有jdk动态代理与责任链模式的完美结合都是我们在项目实践中榜样。当然今天我们主要不是复习,而是学习mybatis的缓存。我们在第二篇文章中说mybatis有缓存,但是我们并没有进行详细的说明,在此我们详细的研究一下。
我们在业务中经常需要注入mybatis的mapper进行数据库的操作,但是因为业务不同我们经常要将相同的mapper注入的不同的业务层中去,但是我们说过我们注入的mapper其实本质是一个sqlsession的实体。基于这一点我们知道mybatis缓存也就是基于会话的,如果是这样,那么如果不同的会话对数据库的修改是不是就会产生脏数据的问题。除此之外如果我们使用的是分布式服务,如果我们使用mybatis缓存,不同的机器那么缓存就更乱了。这里我们所说的其实是mybatis的一级缓存。在此我们对照源码看一下一级缓存的相关过程。
public Listquery(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();
//如果缓存的范围是statement,则查询结束之后就清空缓存
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
我们看到这块的逻辑还是先走缓存,如果缓存不存在就直接查库,然后再回写到缓存中。这块我们看一下这个缓存是怎么初始化的。
这块我们看到是在executor的时候进行的初始化,那么也就是说我们的mapper注入之后就创建好了,就是在这个mapper对象中。同时我们注意到我们的一级缓存的实现在BaseExector中,所以一级缓存是直接开启的。
除此之外,作者还发现我们更新、删除、添加等操作最终都调用的是update方法,根据源码看来,这些对库的更改操作都会直接让一级缓存失效。
那么一级缓存如何设置让其不生效。通过代码查看,通过参数发现一级缓存有一个配置cache-scope,其中session表示开启,statement表示失效。这里的失效其实是查询完毕之后清空。
综合上述,mybatis一级缓存是开启的,对库修改等操作会让一级缓存失效,不同的会话会缓存了相同的数据所以修改的时候不会使得会话缓存同步,所以会产生脏数据。
@Override
@Transactional(rollbackFor = Exception.class)
public void test() {
projectInfoPoMapper.selectByPrimaryKey("1");
projectInfoPoMapper.selectByPrimaryKey("1");
}
使用事务注解
不用事务注解
通过在springboot项目中的尝试,发现在开启事务之后可以使用一级缓存,反之则不会使用一级缓存,而且在一级缓存只能再当前线程中使用。那么这里的问题就是会话与线程之间的关系和缓存与事务之间的关系。
首先在有事务的业务中,sql的执行是原子的,也就是他们会在一个会话中执行。也就是其中的sqlsession会使用相同的一个。那么在之前没有使用事务的情况下,sqlsession是不一样的,所以一级缓存跟随sqlsession的回收而回收,所以使用不到一级缓存。
当调用了commit方法之后,缓存就会被清空
至此我们基本知道了对于一级缓存来说,加了事务之后相同事务中多次执行相同的sql其实是走的一级缓存,但是事务提交之后一级缓存就被清理了。
对于一级缓存,同一线程中相同的sql却没有使用一级缓存的原理真的是上边分析的那种吗,我们的sqlsession到底是什么。我们知道我们调用的sql执行都是通过sqlsessiontemplate执行的。那么我们去看一下sqlsessiontemplate的相关逻辑。
我们看到在sqlsessiontemplate初始化的时候会创建sqlsession并且会将sqlsession存放到sessionhander中。下一次获取session的时候就将该session拷贝出来。
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SqlSession sqlSession = getSqlSession(
SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType,
SqlSessionTemplate.this.exceptionTranslator);
try {
Object result = method.invoke(sqlSession, args);
//判断当前的sqlsession是不是开启事务并且是同一个
if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
// force commit even on non-dirty sessions because some databases require
// a commit/rollback before calling close()
sqlSession.commit(true);
}
return result;
} catch (Throwable t) {
Throwable unwrapped = unwrapThrowable(t);
if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
// release the connection to avoid a deadlock if the translator is no loaded. See issue #22
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
sqlSession = null;
Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
if (translated != null) {
unwrapped = translated;
}
}
throw unwrapped;
} finally {
//sqlsession执行完毕之后还要清空
if (sqlSession != null) {
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
判断使用的sqlsesion是否为相同的
public static boolean isSqlSessionTransactional(SqlSession session, SqlSessionFactory sessionFactory) {
notNull(session, NO_SQL_SESSION_SPECIFIED);
notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
//用事务并执行并且用的相同的sqlsesion
return (holder != null) && (holder.getSqlSession() == session);
}
把sqlsession清理掉
public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) {
notNull(session, NO_SQL_SESSION_SPECIFIED);
notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
if ((holder != null) && (holder.getSqlSession() == session)) {
//如果是事务,那么会将holder的数量递减
LOGGER.debug(() -> "Releasing transactional SqlSession [" + session + "]");
holder.released();
} else {
LOGGER.debug(() -> "Closing non transactional SqlSession [" + session + "]");
session.close();
}
}
这里我们看到在session注册的时候其实给当前事务的获取sesion的次数进行了记录。在每次查库的时候都先判断是否为事务,是事务之后先查事务缓存,如果没有则创建并缓存之后返回,并将获取会话的次数加一,执行sql,完毕之后对获取会话的次数减一,完毕之后其他对库操作进入之后从事务缓存中获取会话,然后执行sql,完毕之后会话次数减一,当所有的sql执行完毕。触发事务的后置处理器,然后对sqlsession和和缓存的解绑。至于前置和后置方法那就是spring切面的事情了。但是这个事务处理的过程我们还是很有必要理解的。
总结:mybatis与spring整合,一级缓存在有事务的条件下有效。有效的原因是事务过程中使用的是相同的sqlsession,其中使用了事务缓存机制。配合切面在事务前后做一些处理保证在时候处理结束前后sqlsession与缓存的绑定和解绑。这也是为啥多线程条件下之后线程内部开事务一级缓存有效的原因,当然在事务comit的后都会进行一级缓存的清除。如果不用事务那么每次执行对库操作都会重新申请sqlsession,所有之前的缓存就会失效,这也是。因为没有缓存sqlsession,所以每次都要重新申请。再对数据库做修改类操作(增删改)都会使得一级缓存失效。我们也可以在配置文件中指定是否使用一级缓存,可以通过cache-scope执行。当然在分布式环境或者注入多个mapper的情况下肯定还是有脏数据的潜在问题,所以建议还是不要使用mybatis一级缓存的好,毕竟在同一线程中相同sql并开事务的概率还是比较小,为了及其微小的优化牺牲数据的一致性那是有问题的!