恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Mybatis一级缓存和结合Spring Framework后失效的源码探究
首页
资讯中心
/
Mybatis一级缓存和结合Spring Framework后失效的源码探究
Mybatis一级缓存和结合Spring Framework后失效的源码探究
发布时间:2026/10/21 2:42:56
源码仓库Gitee·GitHub1.在下面的案例中执行两次查询控制台只会输出一次 SQL 查询mybatis-config.xml ?xml version1.0 encodingUTF-8 ? !DOCTYPE configuration PUBLIC -//mybatis.org//DTD Config 3.0//EN http://mybatis.org/dtd/mybatis-3-config.dtd configuration environments defaultdevelopment environment iddevelopment transactionManager typeJDBC/ dataSource typePOOLED property namedriver valuecom.mysql.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/xxx?useUnicodetrueamp;characterEncodingutf-8amp;autoReconnecttrue/ property nameusername valuexxx/ property namepassword valuexxx/ /dataSource /environment /environments mappers mapper resourcecom/hrh/mapper/PersonMapper.xml/ /mappers /configurationPersonMapper.xml ?xml version1.0 encodingUTF-8 ? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.hrh.mapper.PersonMapper resultMap idBaseResultMap typecom.hrh.bean.Person id columnid propertyid jdbcTypeBIGINT/ result columnname propertyname jdbcTypeVARCHAR/ result columnage propertyage jdbcTypeBIGINT/ /resultMap sql idBase_Column_List id, name, age /sql select idlist resultTypecom.hrh.bean.Person select include refidBase_Column_List/ from tab_person /select /mapperpublic interface PersonMapper { ListPerson 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 探究下内部流程1mapper.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); }cacheMapperMethodMapperMethod 包含方法名、类型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; }2MapperMethod#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) { ListE result; //获取参数 Object param method.convertArgsToSqlCommandParam(args); //是否有分页查询 if (method.hasRowBounds()) { RowBounds rowBounds method.extractRowBounds(args); result sqlSession.EselectList(command.getName(), param, rowBounds); } else { result sqlSession.EselectList(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; }4selectList执行了DefaultSqlSession#selectList()public E ListE selectList(String statement, Object parameter) { return this.selectList(statement, parameter, RowBounds.DEFAULT); }public E ListE selectList(String statement, Object parameter, RowBounds rowBounds) { try { //SQL执行的信息resourcexxMapper.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 ListE 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 ListE 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) ListE list (ListE) 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 ListE 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 的 flushCachetrue 时清空缓存 if (queryStack 0 ms.isFlushCacheRequired()) { clearLocalCache(); } ListE list; try { //嵌套查询层数1 queryStack; //从localCache缓存中获取 list resultHandler null ? (ListE) 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 ListE queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { ListE 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 ListPerson 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(); ListString mapperLocations new ArrayList(); mapperLocations.add(classpath*:com/hrh/mapper/*Mapper*.xml); ListResource 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?useUnicodetruecharacterEncodingutf-8autoReconnecttrue); return driverManagerDataSource; } }AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(MyBatisConfig.class); PersonService bean context.getBean(PersonService.class); bean.getList();下面debug进入的步骤跟上面的1、2、3是一致的但第四步却是进入SqlSessionTemplate#selectList()中【SqlSessionTemplate是mybatis-spring-xx.jar的上文的DefaultSqlSession是属于mybatis-xx.jar的】public E ListE 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 管理的没有暴露出来为了稳妥决定直接给你关了。