mybatis缓存有一级缓存和二级缓存,一级缓存的作用域是sqlSession,在一次会话内,默认是开启的,如果在一次会话内,查询的sql、参数相同,则
会从缓存中取数据,执行dml操作会清楚缓存,二级缓存的作用域是sqlSessionFactory,默认是关闭的,需要在mybatis-config.xml指定开启,在一个会话完成后,
会将所有的select的查询数据缓存,其他的会话如果以相同的sql和参数查询,有能够从缓存中拿到结果。
一:一级缓存的使用
测试用例如下:在同一个sqlSession会话内,执行两次相同的查询操作
public static void main(String[] args) throws IOException { // 将mybatis-config的配置文件读入内存,生成字符流对象 Reader reader = Resources.getResourceAsReader("mybatis-config.xml"); // 解析全局配置文件mybatis-config.xml SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); SqlSessionFactory sqlSessionFactory = builder.build(reader); SqlSession sqlSession = sqlSessionFactory.openSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); // PageHelper.startPage(1,2); // 测试一级缓存: Listlist = userMapper.selectUser("hello105"); System.out.println("第一次查询结果:" + list.size()); List list2 = userMapper.selectUser("hello105"); System.out.println("第二次查询结果:" + list2.size()); }
因为一级缓存默认开启,且缓存key值相同,从结果可以看到,第二次没有执行数据库select的操作,直接从缓存拿的数据。
二:从源码层面分析一下一级缓存
1:在看查询缓存之前,我们先来看一下Executor的创建,这个是SqlSessionFactory中的方法,看一下configuration.newExecutor
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对象后,会把它包装成一个cacheExecutor,因为cacheEnabled默认是开启的
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; }
protected boolean cacheEnabled = true;
来看一下构造函数里面的逻辑:
executor = new CachingExecutor(executor);
会把真正的executor维护到cacheExecutor的属性delegate上。
public CachingExecutor(Executor delegate) { this.delegate = delegate; delegate.setExecutorWrapper(this); }
2:executor的创建完成后,我们来看一下怎么缓存的,来到DefaultSqlSession类,因为要执行查询操作
publicList 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(); } }
这里会根据sql语句、参数、命名空间生成一个缓存key值
publicList 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); }
调用query方法,首先会判断mapper.xml或者mapper类上是否开启了二级缓存(sqlSessionFactory全局),这里我们没有开启,会跳过if语句,直接执行查询,delegate.query
就是创建execcutor的时候封装进去的SimpleExecutor对象
publicList 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, parameterObject, boundSql); @SuppressWarnings("unchecked") List list = (List ) tcm.getObject(cache, key); if (list == null) { list = delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); tcm.putObject(cache, key, list); // issue #578 and #116 } return list; } } return delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); }
这里的查询,首先会到一级缓存localCache,第一次查询localCache中没有数据,返回null,然后会调用queryFromDatabase接口,去真正的执行数据库的查询操作
publicList 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 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(); if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { // issue #482 clearLocalCache(); } } return list; }
这个方法,首先设置了一个默认值放入localCache,查询完成后删除key值,然后将查询结果list放入缓存localCache
privateList queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { List 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; }
所以当第二次执行查询操作时,发现key值相同,就会到一级缓存中去查询,这样就会出现日志中查询两次,但是只会执行一次数据库操作的现象了
一级缓存的源码就是这样,下面来看一下二级缓存
三:二级缓存的使用
mybatis-config.xml可以配置也可以不配置
在mapper接口类配置注解或者在mapper.xml文件中配置cache标签
测试用例:创建2个不同的sqlSession,但是查询sql和参数相同
public static void main(String[] args) throws IOException { // 将mybatis-config的配置文件读入内存,生成字符流对象 Reader reader = Resources.getResourceAsReader("mybatis-config.xml"); // 解析全局配置文件mybatis-config.xml SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); SqlSessionFactory sqlSessionFactory = builder.build(reader); SqlSession sqlSession = sqlSessionFactory.openSession(); UserMapper userMapper = sqlSession.getMapper(UserMapper.class); // PageHelper.startPage(1,2); // 测试一级缓存: Listlist = userMapper.selectUser("hello105"); System.out.println("第一次查询结果:" + list.size()); // List list2 = userMapper.selectUser("hello105"); // System.out.println("第二次查询结果:" + list2.size()); // 只有sqlSession关闭时,数据才会缓存到二级缓存 sqlSession.close(); // 测试二级缓存: SqlSession sqlSession2 = sqlSessionFactory.openSession(); UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class); System.out.println("另外一个会话查询结果:"+userMapper2.selectUser("hello105").size()); }
执行结果,只查询了1次,第二次查询的缓存:
看一下注册,mybatis-config.xml中开启时,会在XMLConfigBuilder中解析设置:
不配默认是true:
Mapper类上加注解@CacheNamespace 或者在Mapper.xml中配置Cache标签
看一下解析:XMLMapperBuilder类中解析命名空间:
然后将解析后的元数据封装到Cache对象中,缓存到configuration中
把cache对象 赋值给了currentCache,而在创建MapperStatement的时候会把currentCache赋值给cache属性
public MappedStatement addMappedStatement( String id, SqlSource sqlSource, StatementType statementType, SqlCommandType sqlCommandType, Integer fetchSize, Integer timeout, String parameterMap, Class> parameterType, String resultMap, Class> resultType, ResultSetType resultSetType, boolean flushCache, boolean useCache, boolean resultOrdered, KeyGenerator keyGenerator, String keyProperty, String keyColumn, String databaseId, LanguageDriver lang, String resultSets) { if (unresolvedCacheRef) { throw new IncompleteElementException("Cache-ref not yet resolved"); } id = applyCurrentNamespace(id, false); boolean isSelect = sqlCommandType == SqlCommandType.SELECT; MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType) .resource(resource) .fetchSize(fetchSize) .timeout(timeout) .statementType(statementType) .keyGenerator(keyGenerator) .keyProperty(keyProperty) .keyColumn(keyColumn) .databaseId(databaseId) .lang(lang) .resultOrdered(resultOrdered) .resulSets(resultSets) .resultMaps(getStatementResultMaps(resultMap, resultType, id)) .resultSetType(resultSetType) .flushCacheRequired(valueOrDefault(flushCache, !isSelect)) .useCache(valueOrDefault(useCache, isSelect)) .cache(currentCache); ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id); if (statementParameterMap != null) { statementBuilder.parameterMap(statementParameterMap); } MappedStatement statement = statementBuilder.build(); configuration.addMappedStatement(statement); return statement; }
下面看看具体的查询,其实在分析一级缓存的时候有涉及到,这里我们详细看一下:
MappedStatement的getCache方法,如果配置Cache标签或者注解就不为null
默认用到缓存类型是LruCache:
存取数据
向二级缓存中放数据是在会话关闭这个动作放的
总结:mybatis缓存
在创建executor的时候,会对executor进行包装,包装成cacheExecutor,在DefaultSqlSession中调用query时,
会路由到CacheExecutor的query方法,首先判断二级缓存是否开启,如果开启,则到二级缓存中查询,看是否命中,
命中cacheKey则返回结果,如果没有命中则委托给simpleExecutor处理,simpleExecutor首先会到一级缓存中查询,如果
命中则返回,没有命中则执行数据库查询操作,从数据库查询到结果后把数据放到一级缓存中,当这个sqlSession关闭的
时候会向二级缓存中缓存数据,大致的查询流程就是这样
如果一级和二级缓存都开启,那么在同一个sqlSession内会取一级缓存,其他的sqlSession会到二级缓存中获取
一级缓存 | 二级缓存 | |
作用域 | sqlSession会话内 | sqlSessionFactory全局 |
开启情况 | 默认开启 | 需要在mapper接口或者mapper.xml配置 |