一、mybatis的一级缓存
一级缓存在BaseExecutor的localCache里面。源码如下:
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;
}
可以看到一级缓存localCache被硬编码到查询语句了,如果要关闭一级缓存,那么就需要执行:
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
让isFlushCacheRequired执行为true,然后就可以提前清理掉缓存。而flushCacheRequired这个是配置的,select命令默认true,update/insert/delete默认是false。
简单说说一级缓存一致性问题:
- 一级缓存是session级别,也就是事务级别。mysql认为同一个事务内,两个完全相同的sql查询,第二个应该直接返回第一次的结果即可。不管多线程还是分布式情况下,该数据在数据库服务器上可能已经修改了,但是由于事务存在隔离性,并且mysql默认是可重复读,所以mybatis返回第一次的结果也顺便解决了可重复读问题,也没啥可说的。关键问题是,如果我的数据库的隔离级别是读提交,那么mybatis自作主张的在同一个事务,第二次查询返回第一次的结果,就有问题了。我不需要可重复读,你却给我可重复读,这样的脏读就无法避免了。。。
1.1 疑问1:不使用一级缓存,为啥要维护它?
这里localCache为啥一定要维护它?不想用一级缓存,虽然可以用flushCacheRequired来清理掉,达到不用缓存的目的。但是不用缓存,还要维护它(插入它,下次执行的时候再清除它),不觉得很奇怪吗?为了维护localCache,要占用内存,并且set和get也是要消耗cpu。
为啥不能直接queryFromDatabase,不维护localCache,能节省不少cpu指令和内存呢?
1.2 疑问2:LocalCacheScope为STATEMENT的时候,会清理缓存。但是结果 list已经从localcache拿到了,然后直接返回了。再清理缓存,还有啥意义?如下:
.....
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
list是有可能从前面的localcache先拿到,这里在clearLocalCache,不觉得已经晚了嘛~
这个问题不应该割裂的看,应该从第一次查询来看。第一次查询的时候,list必然为空,然后从db查询出结果放到localCache。放完后,发现是statement级别,需要再清理掉缓存,那么cache又变成了空。第二次查询的时候,虽然优先从缓存中取,但是照样取不到。所以,就没有上面的list拿到结果直接返回给用户的问题了!!!这种编码风格,可还真有点奇葩,不深入想想很容易被表面迷惑。
参考文章:mybatis一级缓存对spring事务隔离级别表现的影响
这篇文章很好的解决了我的疑问。mybatis默认一级缓存是session级别,session对应事务级别。当我们业务显示的申明事务,那么同一个事务中相同sql语句的查询,就会走一级缓存。而mysql数据库的隔离级别是读提交,那么同一个事务中的第二次查询就应该可以读取到其他事务提交的数据,但是mybatis的一级缓存不会再查询数据库了,而是直接返回该事务上一次查询的结果。所以,在mysql隔离级别小于重复读且开启了事务,就可能会出现上面不一致的问题。
二、mybatis的二级缓存
二级缓存用的是CachingExecutor,采用装饰模式,装饰了BaseExecutor。它的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);
}
二级缓存用的tcm对象,从代码也能看到优先tcm缓存,如果找到了就不走BaseExecutor,直接返回。tcm是namespace维度,不是sqlSession维度,所以可以大范围共享,但是不一致问题更突出。
二级缓存,没开启就不会创建tcm缓存,也不会多执行tcm的get set等多余的指令,很干净,很优雅。
实在不懂一级缓存,不管用不用都要先创建localcache,然后set,get,再清除。。。
四、每条sql执行完成后,sqlSession是如何关闭的呢?
源码在类:org.mybatis.spring.SqlSessionTemplate.SqlSessionInterceptor

可以看到这里的close和事务没有关系呀。这里只要sql执行完成,就一定会关闭session。而一个事务会有多个sql语句。。。又搞不懂了
OK,这里来解释下:
从上面的finally来看确实是,但是我们进入closeSqlSession方法去看下:
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)) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Releasing transactional SqlSession [" + session + "]");
}
holder.released();
} else {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Closing non transactional SqlSession [" + session + "]");
}
session.close();
}
}
重点来了,这里引入了事务概念,先从事务管理器拿holder。如果拿到了,说明当前sql的执行是在事务里面,那么只需要执行holder.released(),无需close掉session。如果没有事务,那么当前sql执行完就马上close掉事务。
我们继续思考,这里holder是怎么设置进去的呢?是不是在执行sql前面,mybatis会去判断当前执行是否在事务里面,如果在就像事务管理器添加一个holder,如果不在创建一个新的session给该sql执行。带着疑问,我们向上看invoke方法刚开始的一段代码:
SqlSession sqlSession = getSqlSession(
SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType,
SqlSessionTemplate.this.exceptionTranslator);
我们深入getSqlSession看一下:
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Creating a new SqlSession");
}
session = sessionFactory.openSession(executorType);
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
继续看sessionHolder方法:
private static SqlSession sessionHolder(ExecutorType executorType, SqlSessionHolder holder) {
SqlSession session = null;
if (holder != null && holder.isSynchronizedWithTransaction()) {
if (holder.getExecutorType() != executorType) {
throw new TransientDataAccessResourceException("Cannot change the ExecutorType when there is an existing transaction");
}
holder.requested();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Fetched SqlSession [" + holder.getSqlSession() + "] from current transaction");
}
session = holder.getSqlSession();
}
return session;
}
可以看到这里也关联了事务,先从事务获取session。如果没有找到,会调用openSession新建一个session给当前sql执行用。
网友评论