MyBatis源码学习

mybatis 不会直接和数据库进行打交道,mybatis 其实是对 jdbc api 的进一步封装,最终和数据库打交道的仍然是 jdbc 。

1. MyBatis基本构成

  • SqlSessionFactoryBuilder(构造器):它会根据配置信息或者代码来生成SqlSessionFactory(工厂接口);
    • 生命周期:它的作用就是一个构造器,一旦我们构建了SqlSessionFactory,SqlSessionFactoryBuilder的作用就已经完结。所以它的生命周期仅存在于方法局部。
  • SqlSessionFactory:依靠工厂来生成SqlSession(会话);
    • 生命周期:SqlSessionFactory的作用是创建SqlSession,而SqlSession就是一个会话,相当于JDBC中的Connection对象。每次应用程序需要访问数据库,我们就通过SqlSessionFactory创建SqlSession,所以SqlSessionFactory应该在MyBatis应用的整个生命周期中。而如果我们多次创建同一个数据库的SqlSessionFactory,则每次创建SqlSessionFactory会打开更多的数据库连接(Connection)资源,那么连接资源就很快会被耗尽。因此SqlSessionFactory是一个全局单例,对应一个数据库连接池。
  • SqlSession:是一个既可以发送SQL去执行并返回结果,也可以获取Mapper的接口。
    • 生命周期:SqlSession相当于一个JDBC的Connection对象,在一次请求事务会话后,我们会将其关闭。
  • SQL Mapper:它是由一个Java接口和XML文件(或注解)构成的,需要给出对应的SQL和映射规则。它负责发送SQL去执行,并返回结果;
    • 生命周期:Mapper的作用是发送SQL,然后返回我们需要的结果,因此它应该在一个SqlSession事务方法之内,是一个方法级别的东西,声明周期与SqlSession相同。

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();
  }
}
image.png

再来看一下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不会清空缓存,所以修改引用后,下次查询得到的结果会有问题

image.png

去掉@Transactional后变得正常了
image.png

其实SqlSession 一级缓存的查询工作流程为:

  1. 对于某个查询,根据statementId, params, rowBounds来构建一个key值,根据这个key值去缓存Cache中取出对应的key值存储的缓存结果;
  2. 判断从Cache中根据特定的key值取的数据数据是否为空,即是否命中;
  3. 如果命中,则直接将缓存结果返回;
  4. 如果没命中:
    a. 去数据库中查询数据,得到查询结果;
    b. 将key和查询到的结果分别作为key,value对存储到Cache中;
    c. 将查询结果返回;

MyBatis认为,对于两次查询,如果以下条件都完全一样,那么就认为它们是完全相同的两次查询:

  1. 传入的 statementId
  2. 查询时要求的结果集中的结果范围 (结果的范围通过rowBounds.offset和rowBounds.limit表示);
  3. 这次查询所产生的最终要传递给JDBC java.sql.Preparedstatement的Sql语句字符串(boundSql.getSql() )
  4. 传递给java.sql.Statement要设置的参数值

现在分别解释上述四个条件:

  1. 传入的statementId,对于MyBatis而言,你要使用它,必须需要一个statementId,它代表着你将执行什么样的Sql;
  2. 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两级缓存

  1. 默认开启一级缓存,PerpetualCache对象就是使用HashMap来做的(一级缓存只是相对于SqlSession而言
  2. 在参数和SQL完全一样的情况下,我们使用同一个SqlSession对象调用同一个Mapper的方法,往往只执行一次SQL,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession都只会取出当前缓存的数据,而不会再次发送SQl到数据库。
  3. 如果使用不同的SqlSession对象,因为不同的SqlSession都是相互隔离的,所以用相同的Mapper、参数和方法,会发送多次SQL到数据库。
  4. 二级缓存是在Mapper级别的,默认是不开启的,且要求返回的POJO必须是可序列化的,即实现Serializable接口。实现Serializable接口主要是因为缓存不一定是在内存中,也可能在磁盘中,所以需要进行序列化和反序列化。(开启二级缓存后默认insert、update、delete会刷新缓存,缓存使用LRU或FIFO等最近最少使用算法来回收)
  5. 二级缓存是SqlSessionFactory层面的,生命周期与SqlSessionFactory、Configuration对象相同
  6. 默认系统缓存是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对象不同


image.png

image.png

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 二级缓存的缺点

  1. 容易出现脏数据:
    a. 由于二级缓存是Mapper级别的,如UserMapper中出现了查询Role表的SQL,因为RoleMapper与UserMapper的二级缓存不同,所以使用RoleMapper更新Role表并不会刷新UserMapper中查询Role表的SQL;
    b. 同理,当在Mapper中出现关联查询时,其他Mapper修改了关联的数据表,则一定会出现脏数据;
  2. 缓存粒度只到Mapper,无法获取更细粒度的缓存;
  3. 分布式场景下,必然会出现脏数据;
    a. 一级缓存如果为开启事务,则每一个sql对应一个SqlSession对应一个一级缓存,所以不会出现脏数据;如果开启了事务,则在两次查询的间隙有他人修改,可能会出现脏数据(未强制加锁);
  4. 所以说使用二级缓存,还不如自己在业务层做一次缓存;

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查询的结果转换为相应对象的?

  1. 当调用SimpleExecutor#query()方法时,如果没有命中缓存,则会queryFromDatabase,最终会使用如PreparedStatement执行查询;
  2. 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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容