zoukankan      html  css  js  c++  java
  • Mybatis一级缓存和结合Spring Framework后失效的源码探究

      1.在下面的案例中,执行两次查询控制台只会输出一次 SQL 查询:

    mybatis-config.xml
    
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE configuration
            PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
        <environments default="development">
            <environment id="development">
                <transactionManager type="JDBC"/>
                <dataSource type="POOLED">
                    <property name="driver" value="com.mysql.jdbc.Driver"/>
                    <property name="url" value="jdbc:mysql://localhost:3306/xxx?useUnicode=true&amp;characterEncoding=utf-8&amp;autoReconnect=true"/>
                    <property name="username" value="xxx"/>
                    <property name="password" value="xxx"/>
                </dataSource>
            </environment>
        </environments>
        <mappers>
            <mapper resource="com/hrh/mapper/PersonMapper.xml"/>
        </mappers>
    </configuration>
    PersonMapper.xml
    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
    <mapper namespace="com.hrh.mapper.PersonMapper">
        <resultMap id="BaseResultMap" type="com.hrh.bean.Person">
            <id column="id" property="id" jdbcType="BIGINT"/>
            <result column="name" property="name" jdbcType="VARCHAR"/>
            <result column="age" property="age" jdbcType="BIGINT"/>
        </resultMap>
        <sql id="Base_Column_List">
        id, name, age
        </sql>
        <select id="list" resultType="com.hrh.bean.Person">
            select
            <include refid="Base_Column_List"/>
            from tab_person
        </select>
    </mapper>
    public interface PersonMapper {
         List<Person> list();
    }
            String resource = "mybatis-config2.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            SqlSessionFactory sqlSessionFactory =
                    new SqlSessionFactoryBuilder().build(inputStream);
            SqlSession sqlSession = sqlSessionFactory.openSession();//开启会话
            PersonMapper mapper = sqlSession.getMapper(PersonMapper.class);
            mapper.list();
            mapper.list();

     

       之所以会出现这种情况,是因为 Mybatis 存在一级缓存导致的,下面 debug 探究下内部流程:

      (1)mapper.list() 会进入 MapperProxy#invoke():参数 proxy是一个代理对象(每个 Mapper 接口都会被转换成一个代理对象),里面包含会话 sqlSession、接口信息、方法信息;method 是目标方法(当前执行的方法),它里面包含了所属的哪个类(接口)、方法名、返回类型(List、Map、void 或其他)、参数类型等;args 是参数;

      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
          if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
          } else if (isDefaultMethod(method)) {
            return invokeDefaultMethod(proxy, method, args);
          }
        } catch (Throwable t) {
          throw ExceptionUtil.unwrapThrowable(t);
        }
        //从方法缓存methodCache中获取到方法的信息:比如方法名、类型(select、update等)、返回类型
        //如果获取中没有MapperMethod,则创建一个并放入methodCache中
        final MapperMethod mapperMethod = cachedMapperMethod(method);
        //执行查询SQL并返回结果
        return mapperMethod.execute(sqlSession, args);
      }

      

      cacheMapperMethod:MapperMethod 包含方法名、类型(select、update等)、返回类型等信息

      private MapperMethod cachedMapperMethod(Method method) {
        //缓存中获取
        MapperMethod mapperMethod = methodCache.get(method);
        //没有则创建一个对象并放入缓存中供下次方便取用
        if (mapperMethod == null) {
          mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
          methodCache.put(method, mapperMethod);
        }
        return mapperMethod;
      }

      (2)MapperMethod#execute() 根据 SQL 类型进入不同的查询方法

      public Object execute(SqlSession sqlSession, Object[] args) {
        //返回结果
        Object result;
        //判断语句类型
        switch (command.getType()) {
          case INSERT: {//插入语句
          Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.insert(command.getName(), param));
            break;
          }
          case UPDATE: {//更新语句
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.update(command.getName(), param));
            break;
          }
          case DELETE: {//删除语句
            Object param = method.convertArgsToSqlCommandParam(args);
            result = rowCountResult(sqlSession.delete(command.getName(), param));
            break;
          }
          case SELECT://查询语句
            //返回空的查询
            if (method.returnsVoid() && method.hasResultHandler()) {
              executeWithResultHandler(sqlSession, args);
              result = null;
              //返回List的查询
            } else if (method.returnsMany()) {
              result = executeForMany(sqlSession, args);
              //返回Map的查询
            } else if (method.returnsMap()) {
              result = executeForMap(sqlSession, args);
              //返回游标的查询
            } else if (method.returnsCursor()) {
              result = executeForCursor(sqlSession, args);
            } else {
              Object param = method.convertArgsToSqlCommandParam(args);
              result = sqlSession.selectOne(command.getName(), param);
            }
            break;
          case FLUSH:
            result = sqlSession.flushStatements();
            break;
          default:
            throw new BindingException("Unknown execution method for: " + command.getName());
        }
        if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
          throw new BindingException("Mapper method '" + command.getName() 
              + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
        }
        return result;
      }

      (3)上面的案例是 select 语句,返回结果是List集合,所以进入 MapperMethod#executeForMany()

      private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
        List<E> result;
        //获取参数
        Object param = method.convertArgsToSqlCommandParam(args);
        //是否有分页查询
        if (method.hasRowBounds()) {
          RowBounds rowBounds = method.extractRowBounds(args);
          result = sqlSession.<E>selectList(command.getName(), param, rowBounds);
        } else {
          result = sqlSession.<E>selectList(command.getName(), param);
        }
        // issue #510 Collections & arrays support
        //如果list中的泛型跟结果类型不一致,进行转换
        if (!method.getReturnType().isAssignableFrom(result.getClass())) {
          if (method.getReturnType().isArray()) {
            return convertToArray(result);
          } else {
            return convertToDeclaredCollection(sqlSession.getConfiguration(), result);
          }
        }
        return result;
      }

      (4)selectList 执行了 DefaultSqlSession#selectList()

      public <E> List<E> selectList(String statement, Object parameter) {
        return this.selectList(statement, parameter, RowBounds.DEFAULT);
      }
      public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        try {
          //SQL执行的信息:resource(xxMapper.xml)、id、sql、返回类型等
          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();
        }
      }

     

      (5)接下来调用缓存执行器的方法:CachingExecutor#query()

      public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
        //获取到执行SQL
        BoundSql boundSql = ms.getBoundSql(parameterObject);
        //将SQL包装成一个缓存对对象,该对象和结果集组成键值对存储到缓存中,方便下次直接从缓存中拿而不需要再次查询
        //createCacheKey:调用BaseExecutor#createCacheKey
        CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
        return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
      }
      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);
      }

      (6)接下来执行 BaseExecutor#query():从下面可以看到将结果缓存到 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.");
        }
        //如果不是嵌套查询(默认为0),且 <select> 的 flushCache=true 时清空缓存
        if (queryStack == 0 && ms.isFlushCacheRequired()) {
          clearLocalCache();
        }
        List<E> list;
        try {
         //嵌套查询层数+1
          queryStack++;
          //从localCache缓存中获取
          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;
        //缓存中添加占位符
        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;
      }

      2.但当 Spring Framework + Mybatis 时,情况就不一样了,每次查询都会连接数据库查询,控制台都会打印 SQL 出来,如下案例:

    @Service
    public class PersonService  {
        @Autowired
        PersonMapper personMapper;
    
        public List<Person> getList() {
            personMapper.list();
            personMapper.list();
            return personMapper.list();
        }
    }
    @Configuration
    @ComponentScan("com.hrh")
    @MapperScan("com.hrh.mapper")
    public class MyBatisConfig {
        @Bean
        public SqlSessionFactoryBean sqlSessionFactory() throws Exception {
            SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
            factoryBean.setDataSource(dataSource());
            factoryBean.setMapperLocations(resolveMapperLocations());
            return factoryBean;
        }
    
        public Resource[] resolveMapperLocations() {
            ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
            List<String> mapperLocations = new ArrayList<>();
            mapperLocations.add("classpath*:com/hrh/mapper/*Mapper*.xml");
            List<Resource> resources = new ArrayList();
            if (mapperLocations != null) {
                for (String mapperLocation : mapperLocations) {
                    try {
                        Resource[] mappers = resourceResolver.getResources(mapperLocation);
                        resources.addAll(Arrays.asList(mappers));
                    } catch (IOException e) {
                        // ignore
                    }
                }
            }
            return resources.toArray(new Resource[resources.size()]);
        }
    
        @Bean
        public DataSource dataSource() {
            DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
            driverManagerDataSource.setDriverClassName("com.mysql.jdbc.Driver");
            driverManagerDataSource.setUsername("xxx");
            driverManagerDataSource.setPassword("xxx");
            driverManagerDataSource.setUrl("jdbc:mysql://localhost:3306/xxx?useUnicode=true&characterEncoding=utf-8&autoReconnect=true");
            return driverManagerDataSource;
        }
    }
            AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MyBatisConfig.class);
            PersonService bean = context.getBean(PersonService.class);
            bean.getList();

     

      下面debug进入的步骤跟上面的(1)、(2)、(3)是一致的,但第四步却是进入 SqlSessionTemplate#selectList() 中【SqlSessionTemplatemybatis-spring-xx.jar的,上文的DefaultSqlSession是属于mybatis-xx.jar的】:

      public <E> List<E> selectList(String statement, Object parameter) {
        return this.selectList(statement, parameter, RowBounds.DEFAULT);
      }

      接下来的 selectList() 会被方法拦截:method.invoke() 会执行到 DefaultSqlSession#selectList(),重新回到上文的第四步并且继续下去,也就是在上文的(1)~(6)中插入了前后文,在其中做了关闭会话的操作;

      private class SqlSessionInterceptor implements InvocationHandler {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          //得到会话
          SqlSession sqlSession = getSqlSession(
              SqlSessionTemplate.this.sqlSessionFactory,
              SqlSessionTemplate.this.executorType,
              SqlSessionTemplate.this.exceptionTranslator);
          try {
            //执行方法查询
            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) {
              closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
            }
          }
        }
      }

     

    总结:

      Mybatis 的一级缓存是会话级别的缓存(单线程的,特别鸡肋),Mybatis 每创建一个 SqlSession 会话对象,就表示打开一次数据库会话,在一次会话中,应用程序很可能在短时间内反复执行相同的查询语句,如果不对数据进行缓存,则每查询一次就要执行一次数据库查询,这就造成数据库资源的浪费。又因为通过 SqlSession 执行的操作,实际上由 Executor 来完成数据库操作的,所以在 Executor 中会建立一个简单的缓存,即一级缓存;将每次的查询结果缓存起来,再次执行查询的时候,会先查询一级缓存(默认开启的),如果命中,则直接返回,否则再去查询数据库并放入缓存中。

      一级缓存的生命周期与 SqlSession 的生命周期相同,因此当 Mybatis 和 Spring Framework 的集成包中扩展了一个 SqlSessionTemplate 类(它是一个代理类,增强了查询方法),所有的查询经过 SqlSessionTemplate 代理拦截后再进入到 DefaultSqlSession#selectList() 中,结束查询后把会话SqlSession 关了,所以导致了缓存失效。

      那为什么要这么操作呢?

      原始的 Mybatis 有暴露 SqlSession 接口,因此有 close 方法暴露出来供你选择使用,你可以选择关与不关,但在 Mybatis 和 Spring Framework 的集成包中,SqlSession 是交给了 Spring Framework 管理的,没有暴露出来,为了稳妥决定,直接给你关了。

    作者:huangrenhui
    欢迎任何形式的转载,但请务必注明出处。
    如果,您认为阅读这篇博客让您有些收获,不妨点击一下右下角的【推荐】。
    如果,您希望更容易地发现我的新博客,不妨点击一下左下角的【关注我】。
    如果,您对我的博客所讲述的内容有兴趣,请继续关注我的后续博客,我是【码猿手】。
    限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。
  • 相关阅读:
    react-redux
    Vue中常用的UI框架
    vue中router与route的区别
    H5新增input属性
    H5新增的input类型
    菜鸡对作用域链的理解
    自己对路由的一些理解
    浏览器缓存
    黄瓜的不定期更新面试题
    ajax封装
  • 原文地址:https://www.cnblogs.com/huangrenhui/p/14702549.html
Copyright © 2011-2022 走看看