本文基于Mybatis-3.4.6。
简介
-
解决完全相同的重复查询,减少查询数据库的次数,减少资源浪费。MyBatis会在表示会话的SqlSession对象中建立一个简单的缓存,将每次查询到的结果结果缓存起来,当下次查询的时候,如果判断先前有个完全一样的查询,会直接从缓存中直接将结果取出,返回给用户,不需要再进行一次数据库查询了。
-
一级缓存是session级别的数据缓存。
-
mybatis中sqlsession只是一个对外接口类,用户通过它来和数据库交互。SqlSession实际上将用户的操作委托给Executor执行器来完成,包括各类对数据库的操作。MyBatis会在表示会话的SqlSession对象中建立一个简单的缓存,将每次查询到的结果结果缓存起来,当下次查询的时候,如果判断先前有个完全一样的查询,会直接从缓存中直接将结果取出,返回给用户,不需要再进行一次数据库查询了。
-
mybatis的缓存实现包括了三个对象,分别是SqlSession,Executor对象,PerpetualCache对象。
- SqlSession接口的实现类DefaultSqlSession有一个Executor类型的成员变量。
- Executor接口的抽象实现类BaseExecutor中有两个个PerpetualCache类型的成员变量,其中localCache就是缓存的存储对象
- PerpetualCache的内部有一个HashMap类型的成员变量cache,该对象就是mybatis中,真正的一级缓存的存储对象
- 综上,mybatis中的一次Session查询的所有查询结果,都将被放置BaseExector的localCache成员变量里,在Perpetualcache类型的localCache中的cache成员变量里
生命周期
- 存放一级缓存的cache的初始化过程如下:
sqlSessionFactory.openSession();
从DefaultSqlSessionFactory中获取SqlSession,相关源码如下:public SqlSession openSession() { return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false); } 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); // 注意此处,Executor已经被生成,在SqlSession的生成过程中,作为参数传递到SqlSession中 // newExecutor()源码在下一部分 final Executor executor = configuration.newExecutor(tx, execType); // DefaultSqlSession的创建 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(); } }
new DefaultSqlSession(configuration, executor, autoCommit);
调用DefaultSqlSession的构造函数来新建该对象。源码如下:public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) { this.configuration = configuration; this.executor = executor; this.dirty = false; this.autoCommit = autoCommit; }
configuration.newExecutor(tx, execType)
这一段代码在Configuration中,源码如下:public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; // 根据不同需要创建不同的Executor,但是这些Executor都继承自BaseExecutor 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; }
在源码中,根据方法传入的参数,决定创建不同的Executor类型。但调用的构造函数无一例外,都是BaseExecutor的构造函数,以下代码分别是不同Executor的构造函数。源码如下:
public BatchExecutor(Configuration configuration, Transaction transaction) { super(configuration, transaction); } public ReuseExecutor(Configuration configuration, Transaction transaction) { super(configuration, transaction); } public SimpleExecutor(Configuration configuration, Transaction transaction) { super(configuration, transaction); } 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; }
在BaseExecutor中,对自身的成员变量进行了初始化,其中备受关注的localCache也在其中。其仅仅完成了一个工作,就是给定id并将其传入到实例中。cache自己进行了初始化,被赋值为一个HashMap的实例对象。
-
通过上面的源码分析,基本上能清晰的看到一级缓存的生命周期伴随着SqlSession的生命周期存在。因为它只是SqlSession的一个成员变量,因此不可能抛开SqlSession独立存活。一旦SQLSession被关闭并被释放,一级缓存也将烟消云散。
- SqlSession调用close()方法,Executor中的PerpetualCache成员变量将被释放,一级缓存不可用。源码如下:
public void close() { try { // 释放executor executor.close(isCommitOrRollbackRequired(false)); closeCursors(); dirty = false; } finally { ErrorContext.instance().reset(); } } private boolean isCommitOrRollbackRequired(boolean force) { return (!autoCommit && dirty) || force; } private void closeCursors() { if (cursorList != null && cursorList.size() != 0) { for (Cursor<?> cursor : cursorList) { try { cursor.close(); } catch (IOException e) { throw ExceptionFactory.wrapException("Error closing cursor. Cause: " + e, e); } } cursorList.clear(); } }
释放executor资源,主要是重置其中的成员变量的引用关系。BaseExector源码如下:
public void close(boolean forceRollback) { try { try { // 是否需要强制回滚 rollback(forceRollback); } finally { // 如果事务不为null,就关闭事务 if (transaction != null) { transaction.close(); } } } catch (SQLException e) { // Ignore. There's nothing that can be done at this point. log.warn("Unexpected exception on closing transaction. Cause: " + e); } finally { // 将成员变量的引用关系置为null,方便其对象被回收 transaction = null; deferredLoads = null; localCache = null; localOutputParameterCache = null; closed = true; } }
- SqlSession调用clearCache(),会清空Executor中PerpetualCache成员变量中的数据,但SqlSession仍然可用。DefaultSqlSession源码如下:
public void clearCache() { executor.clearLocalCache(); }
BaseExecutor中的clearLocalCache()方法将清理其两个PerpetualCache类型的成员变量。源码如下:
public void clearLocalCache() { if (!closed) { localCache.clear(); localOutputParameterCache.clear(); } }
PerPetualCache中的clear()方法如下:
扫描二维码关注公众号,回复: 9046014 查看本文章public void clear() { cache.clear(); }
因为cache是HashMap类型的,因此这实际就是调用hashMap的clear方法。
- 在DefaultSQLSession中,insert(),update(),delete()方法的实现,最终都依赖于一个update()方法。源码如下:
public int insert(String statement) { return insert(statement, null); } public int insert(String statement, Object parameter) { return update(statement, parameter); } public int update(String statement) { return update(statement, null); } public int update(String statement, Object parameter) { try { dirty = true; MappedStatement ms = configuration.getMappedStatement(statement); return executor.update(ms, wrapCollection(parameter)); } catch (Exception e) { throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e); } fina 大专栏 Mybatis的一级缓存机制与源码解析lly { ErrorContext.instance().reset(); } } public int delete(String statement) { return update(statement, null); } public int delete(String statement, Object parameter) { return update(statement, parameter); }
仔细观察会发现insert,update,delete都是两组方法,但是无论是哪一种操作,最后都通用了
update(statement, parameter);
。在该方法中,使用Executor的update()方法区完成数据库更新操作,这再一次印证了前文说到的,SqlSession所有的数据操作都被委托给Executor完成。下面展示BaseExecutor的update方法:public int update(MappedStatement ms, Object parameter) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId()); if (closed) { throw new ExecutorException("Executor was closed."); } // 该方法在上文有展示源码,就是去清理Executor两个本地PerpetualCache类型的成员变量中的HashMap中的数据 clearLocalCache(); return doUpdate(ms, parameter); }
- 从上一部分的源码我们能得出结论,mybatis中插入,修改,删除,都会对一级缓存进行清空,注意仅仅是清空数据,没有释放缓存对象!
工作原理
- 一级缓存的工作原理如下:
- mybatis中的所有查询操作,最终都会调用SqlSession中的
selectList(String statement, Object parameter, RowBounds rowBounds)
来完成,源码展示如下:public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); // 此处再次印证前文所说的,SqlSession对于数据库的所有操作,都是委托给Executor去完成的 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(); } }
- 注意其中的
executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
该方法中隐藏了一级缓存的实现细节,源码如下:public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); } 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++; // 根据key值从缓存中获取值 list = resultHandler == null ? (List<E>) localCache.getObject(key) : null; if (list != null) { handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else { // 如果缓存中的结果为null,则去查询数据库。源码下面 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; } 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; }
通过以上源码,大致能看懂一级缓存的实现路径。在查询过程中,首先去读取缓存中,如果缓存中没有,就读取数据库,并将数据库结果放入到缓存。
- Mybatis的SqlSession一级缓存的工作流程
- 对于某个查询,根据statementId,params,rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果;
- 判断从Cache中根据特定的key值取的数据数据是否为空;
- 如果不为空就直接返回
- 如果为空就去查询数据库,将结果放入到缓存中,返回结果
- mybatis中的所有查询操作,最终都会调用SqlSession中的
- mybatis通过以下内容相同,则确定两次查询是相同的查询:
- 传入的statementId相同
- 查询时要求的结果集中的结果范围(结果的范围通过rowBounds.offset和rowBounds.limit表示)相同。
- 这次查询所产生的最终要传递给JDBC java.sql.Preparedstatement的Sql语句字符串(boundSql.getSql())相同
- 传递给java.sql.Statement要设置的参数值
- 传入的statementId,对于MyBatis而言,你要使用它,必须需要一个statementId,它代表着你将执行什么样的Sql。MyBatis自身提供的分页功能是通过RowBounds来实现的,它通过rowBounds.offset和rowBounds.limit来过滤查询出来的结果集,这种分页功能是基于查询结果的再过滤,而不是进行数据库的物理分页。条件一和条件二是Mybatis自身的一个判定。由于MyBatis底层还是依赖于JDBC实现的,那么,对于两次完全一模一样的查询,MyBatis要保证对于底层JDBC而言,也是完全一致的查询才行。而对于JDBC而言,两次查询,只要传入给JDBC的SQL语句完全一致,传入的参数也完全一致,就认为是两次查询是完全一致的。上面的第三第四个条件都符合的总结就是,调用JDBC的时候,传入的SQL语句要完全相同,传递给JDBC的参数值也要完全相同。
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; }
- 对于数据变化频率很大,并且需要高时效准确性的数据要求,我们使用SqlSession查询的时候,要控制好SqlSession的生存时间,SqlSession的生存时间越长,它其中缓存的数据有可能就越旧,从而造成和真实数据库的误差;同时对于这种情况,用户也可以手动地适时清空SqlSession中的缓存。对于只执行、并且频繁执行大范围的select操作的SqlSession对象,SqlSession对象的生存时间不应过长。