mybatis 不会直接和数据库进行打交道,mybatis 其实是对 jdbc api 的进一步封装,最终和数据库打交道的仍然是 jdbc 。
1. MyBatis基本构成
- SqlSessionFactoryBuilder(构造器):它会根据配置信息或者代码来生成SqlSessionFactory(工厂接口);
- 生命周期:它的作用就是一个构造器,一旦我们构建了SqlSessionFactory,SqlSessionFactoryBuilder的作用就已经完结。
所以它的生命周期仅存在于方法局部。
- 生命周期:它的作用就是一个构造器,一旦我们构建了SqlSessionFactory,SqlSessionFactoryBuilder的作用就已经完结。
- SqlSessionFactory:依靠工厂来生成SqlSession(会话);
- 生命周期:SqlSessionFactory的作用是创建SqlSession,而SqlSession就是一个会话,相当于JDBC中的Connection对象。每次应用程序需要访问数据库,我们就通过SqlSessionFactory创建SqlSession,
所以SqlSessionFactory应该在MyBatis应用的整个生命周期中
。而如果我们多次创建同一个数据库的SqlSessionFactory,则每次创建SqlSessionFactory会打开更多的数据库连接(Connection)资源,那么连接资源就很快会被耗尽。因此SqlSessionFactory是一个全局单例,对应一个数据库连接池。
- 生命周期:SqlSessionFactory的作用是创建SqlSession,而SqlSession就是一个会话,相当于JDBC中的Connection对象。每次应用程序需要访问数据库,我们就通过SqlSessionFactory创建SqlSession,
- SqlSession:是一个既可以发送SQL去执行并返回结果,也可以获取Mapper的接口。
- 生命周期:SqlSession相当于一个JDBC的Connection对象,
在一次请求事务会话后,我们会将其关闭。
- 生命周期:SqlSession相当于一个JDBC的Connection对象,
- SQL Mapper:它是由一个Java接口和XML文件(或注解)构成的,需要给出对应的SQL和映射规则。它负责发送SQL去执行,并返回结果;
- 生命周期:Mapper的作用是发送SQL,然后返回我们需要的结果,因此它应该在
一个SqlSession事务方法之内,是一个方法级别的东西,声明周期与SqlSession相同。
- 生命周期:Mapper的作用是发送SQL,然后返回我们需要的结果,因此它应该在
2. SqlSessionFactoryBuilder
通过XMLConfigBuilder解析配置的XML文件,读出配置参数并存入Configuration类中。(MyBatis中几乎所有的配置都是存在这里的)
使用了建造者模式,主要过程就是解析xml配置文件,new SqlSessionFactory
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
/**
* 1. 根据inputStream等信息创建XMLConfigBuilder对象;
* 2. XMLConfigBuilder会将XML配置文件的信息转换为Document对象;
*/
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
/**
* 3. parser.parse()会处理每个node并返回Configuration对象,解析此Node节点的子Node,获取相关属性:properties, settings, typeAliases,typeHandlers,
* objectFactory, objectWrapperFactory, plugins, environments,databaseIdProvider, mappers
* 4. 如解析typeHandlers的过程就是将TypeHandler.class注册到一个hashmap中
* 5. 最后调用build方法 new DefaultSqlSessionFactory(config)
*/
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
3. SqlSessionFactory
使用Configuration对象去创建SqlSessionFactory(默认的实现为DefaultSqlSessionFactory)。
主要涉及二级缓存的Executor,直接new DefaultSqlSession
public SqlSession openSession(ExecutorType execType) {
return openSessionFromDataSource(execType, 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);
/**
* new Transaction, transactionFactory的实现类有JdbcTransactionFactory, ManagedTransactionFactory,SpringManagedTransactionFactory
* Spring与MyBatis整合后使用SpringManagedTransactionFactory,将事务委托给Spring
*/
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
/**
* 创建Executor(事务包含在其中)
* 如果开启了二级缓存,会创建CachingExecutor,一个装饰器
* 先去SqlSessionFactory级别的二级缓存中查,如果查到就使用,查不到则调用原有Executor的查询方法
*/
final Executor executor = configuration.newExecutor(tx, execType);
// 直接new 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();
}
}
4. SqlSession
流程:
SqlSession -> Executor -> StatementHandler(调用prepare方法进行预编译) -> ParameterHandler(设置预编译sql的参数) -> StatementHandler调用PreparedStatement执行sql -> ResultHandler封装结果
- Mapper映射是通过动态代理实现的,在MapperProxy中会根据SQL的类型(insert、update、delete、select)调用SqlSession的对应方法;
- SqlSession中的insert、update、delete、select方法实际上是调用Executor的对应方法;
- SqlSession下的四大对象
- Executor代表执行器,由它来调度StatementHandler、ParameterHandler、ResultHandler等来执行对应的SQL;
- StatementHandler的作用是使用数据库的Statement(PreparedStatement(预编译的,效率高,可以使用占位符代替参数从而多次执行))来执行sql操作;
- ParameterHandler用于SQL对参数的处理,使用TypeHandler向PreparedStatement中设置参数;
- ResultHandler是进行最后数据集(ResultSet)的封装返回的处理;
4.1 select
这里以DefaultSqlSession为例。
DefaultSqlSession中有多个select方法,如selectOne, selectMap, selectList等,但都是以selectList为基础,如selectOne是调用selectList,然后list.get(0)得到结果。selectMap也是先selectList,然后遍历得到的list,转换为Map。
所以我们来看一下selectList
@Override
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
/**
* 根据statement获取MappedStatement
* statement其实就是MappedStatement的id,为被调用Mapper的类名,如com.example.shiro.mapper.UserMapper.selectByPrimaryKey,见下图
*/
MappedStatement ms = configuration.getMappedStatement(statement);
/**
* 调用之前创建的Executor query方法,没有开启二级缓存则使用BaseExecutor
* 注意这里传入的ResultHandler为null,
* 在后续query过程中,如果ResultHandler为null则先尝试从缓存中取,
* 如果ResultHandler不为null则不会尝试从缓存中取
*/
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();
}
}
再来看一下BaseExecutor#query()方法
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
/**
* BoundSql中包含需要动态生成的sql语句,以及对应的参数
*/
BoundSql boundSql = ms.getBoundSql(parameter);
/**
* 根据statementId, params, rowBounds来构建一个key值,MyBatis认为这几个参数能够代表同一个sql
*/
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
@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.");
}
// 当queryStack == 0时才清空缓存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
// 保证在执行过程中不会清空缓存
queryStack++;
/**
* localCache一级缓存,内部为一个HashMap,线程不安全的
* resultHandler DefaultSqlSession中传入的ResultHandler为null
* 注意:这里从cache中获取到的结果强转为list,queryFromDatabase会先传入一个占位符,如果此时有另一个线程进来,再强转则会抛异常,相当于做了多线程操作的处理
*/
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;
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
/**
* 在缓存中放入一个占位符 enum类型
* 当第一个线程正常向数据库中查询时,第二个线程也执行了相同的查询
* 在BaseExecutor#query方法中(List<E>) localCache.getObject(key),此时报类型转换异常,防止多线程操作缓存
*/
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
/**
* 调用StatementHandler#query方法
* 如实现类PreparedStatementHandler,就是调用PreparedStatement#excute方法
*/
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;
}
可以看到,在queryFromDatabase中直接返回了缓存结果的引用,这样就会出现脏读,如下代码:
在一次查询后,修改查询结果,下一次查询时直接从缓存中查询,结果会发现第二次的查询结果也被修改了
@Transactional(rollbackFor = Exception.class)
public User getUser(Long id) {
User user = userMapper.selectByPrimaryKey(id);
log.info("User: {}", user.getUsername());
user.setUsername("test-mybatis-cache");
User user2 = userMapper.selectByPrimaryKey(id);
log.info("User2: {}", user2.getUsername());
return user;
}
缓存中返回的引用,在一次事务中MyBatis不会清空缓存,所以修改引用后,下次查询得到的结果会有问题
去掉@Transactional后变得正常了
其实SqlSession 一级缓存的查询工作流程为:
- 对于某个查询,根据statementId, params, rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果;
- 判断从Cache中根据特定的key值取的数据数据是否为空,即是否命中;
- 如果命中,则直接将缓存结果返回;
- 如果没命中:
a. 去数据库中查询数据,得到查询结果;
b. 将key和查询到的结果分别作为key,value对存储到Cache中;
c. 将查询结果返回;
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底层还是依赖于JDBC实现的,那么,对于两次完全一模一样的查询,MyBatis要保证对于底层JDBC而言,也是完全一致的查询才行。而对于JDBC而言,两次查询,只要传入给JDBC的SQL语句完全一致,传入的参数也完全一致,就认为是两次查询是完全一致的。
上述的第3个条件正是要求保证传递给JDBC的SQL语句完全一致;第4条则是保证传递给JDBC的参数也完全一致;
即3、4两条MyBatis最本质的要求就是:
调用JDBC的时候,传入的SQL语句要完全相同,传递给JDBC的参数值也要完全相同。
4.2 insert & delete
// DefaultSqlSession#insert
@Override
public int insert(String statement) {
return insert(statement, null);
}
@Override
public int insert(String statement, Object parameter) {
return update(statement, parameter);
}
// DefaultSqlSession#delete
@Override
public int delete(String statement) {
return update(statement, null);
}
@Override
public int delete(String statement, Object parameter) {
return update(statement, parameter);
}
可以看到insert和delete方法其实就是调用了update(会清空一级缓存
),下面来看一下update方法
4.3 update
@Override
public int update(String statement) {
return update(statement, null);
}
@Override
public int update(String statement, Object parameter) {
try {
/**
* dirty置为true,在commit和rollback方法中会判断isCommitOrRollbackRequired(),
* 如果dirty为true则表明需要commit,会调用transaction.commit();
*/
dirty = true;
MappedStatement ms = configuration.getMappedStatement(statement);
// 直接调用Executor#update
return executor.update(ms, wrapCollection(parameter));
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
看一下BaseExecutor#update
@Override
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.");
}
/**
* 先清空本地缓存
* localCache.clear();
*/
clearLocalCache();
// doUpdate,一个模板方法,交给子类实现
return doUpdate(ms, parameter);
}
SimpleExecutor#doUpdate
@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
/**
* 创建StatementHandler
* StatementHandler负责处理Mybatis与JDBC之间Statement的交互,如PreparedStatement
*/
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
/**
* 1. 创建JDBC连接Connection(使用连接池)
* 2. 调用StatementHandler#prepare方法,通过Connection创建PreparedStatement
* 3. 调用StatementHandler#parameterize方法,向PreparedStatement中设置sql参数,TypeHandler就是在这里生效的
*/
stmt = prepareStatement(handler, ms.getStatementLog());
/**
* 直接调用PreparedStatement#execute方法,执行sql并返回受影响行数
*/
return handler.update(stmt);
} finally {
/**
* 关闭statement
*/
closeStatement(stmt);
}
}
总体来说,BaseExecutor#update方法比较简单,无非就是先清空本地一级缓存
,再调用PreparedStatement执行sql。
4.4 commit & rollback
@Override
public void commit(boolean force) {
try {
/**
* isCommitOrRollbackRequired(),根据dirty、autoCommit、force判断是否需要提交或回滚
* 先清空缓存,再transaction.commit()
*/
executor.commit(isCommitOrRollbackRequired(force));
/**
* dirty置为false,下次无需提交或回滚
*/
dirty = false;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
@Override
public void rollback(boolean force) {
try {
/**
* isCommitOrRollbackRequired(),根据dirty、autoCommit、force判断是否需要提交或回滚
* 先清空缓存,再transaction.rollback()
*/
executor.rollback(isCommitOrRollbackRequired(force));
/**
* dirty置为false,下次无需提交或回滚
*/
dirty = false;
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error rolling back transaction. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
- 当使用MyBatis-Spring时,在org.mybatis.spring.SqlSessionTemplate中会调用SqlSession#commit,如果添加了@Transactional注解,在commit后sql才会生效,如果有没有添加注解commit前sql就已经生效了。这里无论是否添加了@Transactional注解都需要执行SqlSession#commit主要是考虑到有的数据库必须在close前调用commit或rollback;
- 当使用MyBatis-Spring时,@Transactional注解会使用Spring的事务,则不会调用SqlSession#rollback方法;
5. MyBatis-Spring
文档:http://mybatis.org/spring/zh/getting-started.html
- 一个使用 MyBatis-Spring 的其中一个主要原因是它允许 MyBatis 参与到 Spring 的事务管理中。而不是给 MyBatis 创建一个新的专用事务管理器,MyBatis-Spring 借助了 Spring 中的 DataSourceTransactionManager 来实现事务管理。
- 一旦配置好了 Spring 的事务管理器,你就可以在 Spring 中按你平时的方式来配置事务。并且支持 @Transactional 注解和 AOP 风格的配置。
在事务处理期间,一个单独的 SqlSession 对象将会被创建和使用。当事务完成时,这个 session 会以合适的方式提交或回滚。
- 不能在 Spring 管理的 SqlSession 上调用 SqlSession.commit(),SqlSession.rollback() 或 SqlSession.close() 方法。如果这样做了,就会抛出 UnsupportedOperationException 异常。在使用注入的映射器时,这些方法也不会暴露出来。
-
DefaultSqlSession中的一级缓存就是一个HashMap,它不是线程安全的,MyBatis-Spring中SqlSessionTemplate是线程安全的,它将SqlSession存储在org.springframework.transaction.support.TransactionSynchronizationManager中,TransactionSynchronizationManager中使用ThreadLocal变量保存SqlSession。
每个线程过来都是一个独立的SqlSession,所以能够保证线程安全。https://my.oschina.net/u/3145456/blog/1841572
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal("Transactional resources");
6. SQL Mapper
- 它是由一个Java接口和XML文件(或注解)构成的,需要给出对应的SQL和映射规则。它负责发送SQL去执行,并返回结果;
在Spring启动时(getBean),会初始化所有的Mapper类,并生成对应的代理类MapperProxy。
- 当执行Mapper中的方法时(如userMapper.insert(user)),会调用Mapper的代理类MapperProxy(所以Mapper需要是接口),MapperProxy会调用SqlSessionTemplate的对应方法,如下:
@Override
public int insert(String statement, Object parameter) {
return this.sqlSessionProxy.insert(statement, parameter);
}
- 而这里的sqlSessionProxy是在SqlSessionTemplate的构造函数中创建的动态代理类(主要处理了SqlSession的线程安全问题,最终还是直接调用DefaultSqlSession的对应方法)
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
notNull(executorType, "Property 'executorType' is required");
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
// 创建SqlSession的动态代理类,需要看下SqlSessionInterceptor
this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}
private class SqlSessionInterceptor implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/**
* 1. 尝试去TransactionSynchronizationManager的Threadlocal中寻找SqlSession(线程安全的)
* 1.1 如果能够获取到,则直接返回(此时需要计数器加一,用于记录SqlSession被获取了多少次)
* 1.2 如果获取不到
* 1.2.1 创建新的SqlSession
* 1.2.2 如果当前存在事务,则向TransactionSynchronizationManager的Threadlocal中注册新创建的SqlSession
* 1.2.3 如果没有事务,则不会向TransactionSynchronizationManager注册,所以每次都是新的SQLSession
*/
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
try {
// 可以看到invoke函数的参数中proxy是没有被用到的,这里直接传入的是动态获取的SqlSession
Object result = method.invoke(sqlSession, args);
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 {
if (sqlSession != null) {
/**
* 关闭SqlSession
* 如果TransactionSynchronizationManager中存在SqlSession,减少一个计数(holder.released()),并不直接close SqlSession
* 如果TransactionSynchronizationManager不存在,直接调用SqlSession#close方法
*/
closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
}
}
}
}
- 关闭SqlSession:
- 如果不存在事务,每次执行一个Mapper的方法时,都会创建一个新的SqlSession,执行完毕后关闭;
- 如果存在事务,在事务的过程中,使用的是相同的SqlSession,事务结束后,会关闭SqlSession;
- 也就是说,在事务中才会用到SqlSession的一级缓存,而无事务时,没法触发一级缓存。
7. MyBatis两级缓存
- 默认开启一级缓存,PerpetualCache对象就是使用HashMap来做的(
一级缓存只是相对于SqlSession而言
) -
在参数和SQL完全一样的情况下
,我们使用同一个SqlSession对象调用同一个Mapper的方法,往往只执行一次SQL,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession都只会取出当前缓存的数据,而不会再次发送SQl到数据库。 - 如果使用不同的SqlSession对象,因为不同的SqlSession都是相互隔离的,所以用相同的Mapper、参数和方法,会发送多次SQL到数据库。
- 二级缓存是在Mapper级别的,默认是不开启的,且要求返回的POJO必须是可序列化的,即实现Serializable接口。实现Serializable接口主要是因为缓存不一定是在内存中,也可能在磁盘中,所以需要进行序列化和反序列化。(开启二级缓存后默认insert、update、delete会刷新缓存,缓存使用LRU或FIFO等最近最少使用算法来回收)
- 二级缓存是SqlSessionFactory层面的,生命周期与SqlSessionFactory、Configuration对象相同
- 默认系统缓存是MyBatis所在服务器的本地缓存,如果想使用redis等缓存服务器,MyBatis也支持自定义缓存,需要实现org.apache.ibatis.cache.Cache。
一级缓存的具体实现已经在上面阐述过了,所以这里只讨论下二级缓存。
二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源。
7.1 select
CachingExecutor是一个装饰器,丰富了如SimpleExecutor的功能,提供了二级缓存的支持。
CachingExecutor#query
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
/**
* 1. 从MappedStatement中获取cache
* 1.1 Spring boot中想要开启了二级缓存需要在Mapper上添加@CacheNamespace注解,如果添加了该注解,则这里可以获取到cache
* 1.2 MappedStatement是在启动时,注册到org.apache.ibatis.session.Configuration中的
* 1.2.1 每个Mapper的每个方法都会生成一个MappedStatement,且Mapper中保存了一个cache对象
* a. 如果没有开启二级缓存,cache对象为空
* b. 如果开启了二级缓存,则每个Mapper的不同方法共享同一个cache对象,即mybatis的二级缓存是Mapper级别的
*/
Cache cache = ms.getCache();
if (cache != null) {
/**
* 是否需要刷新缓存,默认情况下,select不需要刷新缓存
*/
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, parameterObject, boundSql);
/**
* tcm.getObject(cache, key)实际上就是去参数中的cache获取缓存
*/
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
/**
* 如果没有查询到,则会去被代理的Executor(如SimpleExecutor)查询
* delegate.query也会去查询一级缓存
*/
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
/**
* 该方法并没有直接将查询的结果对象存储到其封装的二级缓存Cache对象中,
* 而是暂时保存到entriesToAddOnCommit集合中,
* 在事务提交时(CachingExecutor#commit)才会将这些结果从entriesToAddOnCommit集合中添加到二级缓存中
*/
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
/**
* 未开启二级缓存,则直接调用原Executor
*/
return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
下图可以看到,开启了二级缓存后,每个Mapper的不同方法共享同一个cache对象,不同Mapper的cache对象不同
7.2 insert, delete, update
CachingExecutor#update
当SqlSession执行,insert, delete, update时,如果开启了二级缓存会调用CachingExecutor#update方法
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {
//是否需要刷新缓存,insert,delete,update要刷新缓存
flushCacheIfRequired(ms);
// 直接调用原Executor
return delegate.update(ms, parameterObject);
}
开启二级缓存时,SqlSession的声明周期与之前相同:
- 如果不存在事务,每次执行一个Mapper的方法时,都会创建一个新的SqlSession,执行完毕后关闭;
- 如果存在事务,在事务的过程中,使用的是相同的SqlSession,事务结束后,会关闭SqlSession;
7.3 二级缓存的缺点
- 容易出现脏数据:
a. 由于二级缓存是Mapper级别的,如UserMapper中出现了查询Role表的SQL,因为RoleMapper与UserMapper的二级缓存不同,所以使用RoleMapper更新Role表并不会刷新UserMapper中查询Role表的SQL;
b. 同理,当在Mapper中出现关联查询时,其他Mapper修改了关联的数据表,则一定会出现脏数据; - 缓存粒度只到Mapper,无法获取更细粒度的缓存;
-
分布式场景下,必然会出现脏数据;
a. 一级缓存如果为开启事务,则每一个sql对应一个SqlSession对应一个一级缓存,所以不会出现脏数据;如果开启了事务,则在两次查询的间隙有他人修改,可能会出现脏数据(未强制加锁); - 所以说使用二级缓存,还不如自己在业务层做一次缓存;
8. MyBatis延迟加载
当真正使用到这个数据时才会发送sql语句。(级联时,默认将关联的属性都查询出来,如果开启了延迟加载,则使用到关联属性时才会查找)
实现原理:使用动态代理,会生成一个动态代理对象,里面保存着相关的SQL和参数,一旦我们使用这个代理对象的方法,它会进入到动态代理对象的代理方法里,方法里面会通过发送sql和参数,就可以把对应的结果从数据库中查找回来。
MyBatis延迟加载是通过动态代理实现的,当调用配置为延迟加载的属性方法时(如getXXX()),此时会调用动态代理对象的get方法,会发送sql到db查询数据,这些操作是通过SqlSession来执行的。由于在和某些框架集成时,SqlSession的生命周期交给了框架来管理,因此当对象超出SqlSession生命周期调用时,会由于链接关闭等问题而抛出异常。因而在与Spring集成时,需要注意SqlSession的声明周期。
8.1 延迟加载的优缺点
- 优点:先从单表查询,需要时再去查询另一张表(两次查询),如果并没有用到另一张表中的数据,可以加快查询速度。
- 如果使用延迟加载,使用关联查询的话,在数据量很大的情况下,关联查询jion两张大表,效率会很低。而如果延迟加载第二次延迟查询命中索引概率大的话,效率会更高。
-
一定要进行关联查询吗?
- 关联查询效率很低,尤其是两张大表jion的情况下,所以要尽可能避免这种情况!
- 如果非要两张大表jion的话,可以不作为实时场景,让它作为一个定时任务去跑,第一次跑的数据量大可能耗时长,后面采用按时间增量更新的策略,根据时间切分的好后面每次跑的数据量就不会太大。
- 如果一张大表跟一张小表jion,那么可以将小表缓存,单查一张大表将查询得到的结果(肯定比全量jion数据要少)再去jion。
- 大表单查很慢,可以多个线程去查,如果大表做了分库分表或者按时间分区,查询方式就又有不同。
- 尽量将一个大sql拆分为多个小sql,大sql会长时间占用连接,影响其他sql。
- 缺点:因为只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,因为查询工作也要消耗时间,所以可能造成用户等待时间变长,造成用户体验下降。
9. 如何将jdbc查询的结果转换为相应对象的?
- 当调用SimpleExecutor#query()方法时,如果没有命中缓存,则会queryFromDatabase,最终会使用如PreparedStatement执行查询;
- ResultSetHandler#handleResultSets()方法,将resultSet转为对应的对象。
a. 获取resultSet中的所有列名,并获得列名对应的值;
b. 查找是否有匹配的TypeHandler,如果有的话,调用TypeHandler的getResult() → getNullableResult()方法,获得通过TypeHandler转换后的属性值;
c. 利用反射new对象,根据列名获得对象的setXX()方法,再使用反射调用待生成对象的setXX()方法,将属性设置进去;
d. 将设置了属性的对象存入ResultHandler中;
e. 这样仅仅是处理了一条记录,如果查询结果有多条记录,还会循环这个过程。
f. 利用ResultHandler处理对象。
i. 如DefaultResultHandler,内部维护了一个List,查询得到的n条记录都会存在这里
ii. 最终返回的数据会判断List中的个数,如果只有一个就get(0),只返回一个对象。如果有多个会返回这个List