博客
关于我
你凭什么说Spring会导致MyBatis的一级缓存失效
阅读量:342 次
发布时间:2019-03-04

本文共 11552 字,大约阅读时间需要 38 分钟。

一、概述

最近老是听说Spring和MyBtis集成后,一级缓存就不可用了!

我就纳闷了,为什么一级缓存不可用呢?这难道是Spring的BUG?这引起了我极大的兴趣,因为Spring作为一个极其优秀的项目管理框架,它居然也有BUG,我要一探究竟,满足我的好奇心!

二、真的没走缓存

为了帮助我查看源码,我把MyBatis和Spring集成后写了如下代码:

AnnotationConfigApplicationContext annotationConfigApplicationContext;@Beforepublic void init(){    annotationConfigApplicationContext = new AnnotationConfigApplicationContext(AppConfig.class);}@Testpublic void selectTest(){    TestMapper bean = annotationConfigApplicationContext.getBean(TestMapper.class);    List
users = bean.selectUser("周六"); System.out.println(users); List
users1 = bean.selectUser("周六"); System.out.println(users == users1);}复制代码

讲道理,以上代码在常规的环境下,是一定会走一级缓存的,因为它满足一级缓存命中的条件,即同一个 SqlSession 、 StatementId 相同, 参数 相同、 分页条件 相同、 查询语句 相同、 环境名称 相同 六大命中规则,所以理论上,一级缓存是一定会命中的!但是事实上日志如下:

你凭什么说Spring会导致MyBatis的一级缓存失效

 

image-20200707132052562

他居然没有走缓存,而是去查询了两遍数据库,一级缓存华丽丽的的失效了,可是这道理是为什么呢?

三、失效的原因

Spring作为一个顶级项目管理框架,对于如此明显的BUG,他不可能发现不了,即使真的发现不了,那么github上使用者也不可能不提BUG,于是,我打断点调试调试,看下源码就是是如何来操作的!

从哪里下手呢?刚刚我们说过一级缓存的命中规则,2,3,4,5,6条规则一定是一样的,因为我只是单纯的复制了两遍查询,代码上没有变动,所以他的查询语句、参数之类的条件一定是相同的,那么最可能出现的条件就是第一条: 同一个SqlSession ,难道说Spring集成MyBatis后,每一次查询都是用了不同的SqlSession? 以前看过我文章的都应该知道,我之前分析过一篇关于MyBatis设计模式的文章,关于门面模式中说到过: 每一个SqlSession都会有一个唯一的执行器(Executor)与之对应 ,所以说如果想验证是不是同一个SqlSession,只需要验证两次使用的执行器是不是一个就OK了,说做就做,我在 BaseExecutor#query 方法上断点,结果如下:

你凭什么说Spring会导致MyBatis的一级缓存失效

 

image-20200707133723572

果然不出我所料,两次查询走的根本不是一个执行器,那么也就一定不是一个SqlSession,这下只掉原因了,但是为什么呢?

四、罪魁祸首

你凭什么说Spring会导致MyBatis的一级缓存失效

 

image-20200707133958314

通过上图的断点我们可以看出来,正常情况下,我们的Mapper代理里面所包含的应该是 DefaultSqlSession 对象,但是通过整合Spring后我们发现,我们的SqlSession对象被偷梁换柱了,换成了 SqlSessionTemplate 类,我们进入到这个类中:

public class SqlSessionTemplate implements SqlSession, DisposableBean {...}复制代码

发现这个类也继承了 SqlSession 接口,那就好办了,那么查询的方法一定是经过Select方法来实现的,我们进入到他的 selectList 方法,看下他的实现逻辑:

@Overridepublic 
List
selectList(String statement, Object parameter) { return this.sqlSessionProxy.selectList(statement, parameter);}复制代码

我们发现,这个方法内部内部的查询似乎又交给了一层代理,由这一层代理去真正执行的查询操作,我们似乎快找到原因了:

public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,                                 PersistenceExceptionTranslator exceptionTranslator) {    ... 忽略不必要的代码...    this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),        new Class[] { SqlSession.class }, new SqlSessionInterceptor());  }复制代码

果不其然,这个对象在初始化的时候,将这个代理对象也连带着初始化了,这个正是使用的JDK的动态代理来实现的,熟悉动态代理的同学可能会知道,JDK动态代理的精髓也就是 InvocationHandler 的子类,也就是 SqlSessionInterceptor ,我们进入到里面看一下他的实现:

private class SqlSessionInterceptor implements InvocationHandler {    @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {       //获取SqlSession      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)) {          //提交数据          sqlSession.commit(true);        }        //返回查询的数据        return result;      } catch (Throwable t) {        //。。。。忽略不必要代码      } finally {        if (sqlSession != null) {            //关闭SqlSession的连接          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);        }      }    }  }复制代码

既然SqlSession不一致,那么肯定是在获取SqlSession的时候,里面实现了一些逻辑,从而造成了 SqlSession的不一致,我们进入到 getSqlSession 方法中:

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,      PersistenceExceptionTranslator exceptionTranslator) {   //...忽略不必要代码.... //从ThreadLocal变量里面获取当前的SqlSession的处理器    SqlSessionHolder holder =            (SqlSessionHolder)TransactionSynchronizationManager.getResource(sessionFactory); //如果事务同步管理器处于活动状态则从SqlSessionHolder获取Session    SqlSession session = sessionHolder(executorType, holder);    if (session != null) {      return session;    }    //如果SqlSessionHolder中获取的SqlSession为空,则新建一个SqlSession    session = sessionFactory.openSession(executorType); //若事务同步管理器处于活动状态则将SqlSession设置到SqlSessionHolder中保存起来,以便下次使用    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);    return session;  }复制代码

原来如此,原来并不是说Spring使MyBatis的一级缓存失效了,而是因为Spring只有在开启了事务之后,在同一个事务里的SqlSession会被缓存起来,同一个事务中,多次查询是可以命中缓存的!我们回到 SqlSessionInterceptor#invoke 方法里面,他在关闭的SqlSession的时候同样对 是否开启事务做了处理,我们看 closeSqlSession 方法的源码:

public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) {   //........忽略不必要的代码        SqlSessionHolder holder =           (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);    //查看事务同步管理器是否存在 session     if ((holder != null) && (holder.getSqlSession() == session)) {      holder.released();    } else {      //如果不存在就将该Session关闭掉      session.close();    }  }复制代码

那么,既然导致一级缓存失效的罪魁祸首我们找到了,如何解决呢?

五、解决方案

为什么一级缓存失效,因为两次查询没有使用同一个事物,那么我们加上同一个事物,看看情况如何:

@Test    public void selectTest(){        TestMapper bean = annotationConfigApplicationContext.getBean(TestMapper.class);        //添加事务        DataSourceTransactionManager dataSourceTransactionManager =                annotationConfigApplicationContext.getBean(DataSourceTransactionManager.class);        TransactionStatus transaction =                dataSourceTransactionManager.getTransaction(new DefaultTransactionDefinition());                        List
users = bean.selectUser("周六"); System.out.println(users); List
users1 = bean.selectUser("周六"); System.out.println(users == users1); }复制代码

我们这个时候来看一下结果:

你凭什么说Spring会导致MyBatis的一级缓存失效

 

image-20200707141456766

果然不出我所料,一级缓存又被成功的使用上了。

古人云:耳听为虚,眼见为实!只有真正的经历过,才知道哪些是真,哪些是假!这一次调试源码,不光让我对Spring整合MyBatis有了一个整体的认知,更是让我对动态代理有了一个更加深入的了解,后续我会整理一下,分享出来!一、概述

最近老是听说Spring和MyBtis集成后,一级缓存就不可用了!

我就纳闷了,为什么一级缓存不可用呢?这难道是Spring的BUG?这引起了我极大的兴趣,因为Spring作为一个极其优秀的项目管理框架,它居然也有BUG,我要一探究竟,满足我的好奇心!

二、真的没走缓存

为了帮助我查看源码,我把MyBatis和Spring集成后写了如下代码:

AnnotationConfigApplicationContext annotationConfigApplicationContext;@Beforepublic void init(){    annotationConfigApplicationContext = new AnnotationConfigApplicationContext(AppConfig.class);}@Testpublic void selectTest(){    TestMapper bean = annotationConfigApplicationContext.getBean(TestMapper.class);    List
users = bean.selectUser("周六"); System.out.println(users); List
users1 = bean.selectUser("周六"); System.out.println(users == users1);}复制代码

讲道理,以上代码在常规的环境下,是一定会走一级缓存的,因为它满足一级缓存命中的条件,即同一个 SqlSession 、 StatementId 相同, 参数 相同、 分页条件 相同、 查询语句 相同、 环境名称 相同 六大命中规则,所以理论上,一级缓存是一定会命中的!但是事实上日志如下:

你凭什么说Spring会导致MyBatis的一级缓存失效

 

image-20200707132052562

他居然没有走缓存,而是去查询了两遍数据库,一级缓存华丽丽的的失效了,可是这道理是为什么呢?

三、失效的原因

Spring作为一个顶级项目管理框架,对于如此明显的BUG,他不可能发现不了,即使真的发现不了,那么github上使用者也不可能不提BUG,于是,我打断点调试调试,看下源码就是是如何来操作的!

从哪里下手呢?刚刚我们说过一级缓存的命中规则,2,3,4,5,6条规则一定是一样的,因为我只是单纯的复制了两遍查询,代码上没有变动,所以他的查询语句、参数之类的条件一定是相同的,那么最可能出现的条件就是第一条: 同一个SqlSession ,难道说Spring集成MyBatis后,每一次查询都是用了不同的SqlSession? 以前看过我文章的都应该知道,我之前分析过一篇关于MyBatis设计模式的文章,关于门面模式中说到过: 每一个SqlSession都会有一个唯一的执行器(Executor)与之对应 ,所以说如果想验证是不是同一个SqlSession,只需要验证两次使用的执行器是不是一个就OK了,说做就做,我在 BaseExecutor#query 方法上断点,结果如下:

你凭什么说Spring会导致MyBatis的一级缓存失效

 

image-20200707133723572

果然不出我所料,两次查询走的根本不是一个执行器,那么也就一定不是一个SqlSession,这下只掉原因了,但是为什么呢?

四、罪魁祸首

你凭什么说Spring会导致MyBatis的一级缓存失效

 

image-20200707133958314

通过上图的断点我们可以看出来,正常情况下,我们的Mapper代理里面所包含的应该是 DefaultSqlSession 对象,但是通过整合Spring后我们发现,我们的SqlSession对象被偷梁换柱了,换成了 SqlSessionTemplate 类,我们进入到这个类中:

public class SqlSessionTemplate implements SqlSession, DisposableBean {...}复制代码

发现这个类也继承了 SqlSession 接口,那就好办了,那么查询的方法一定是经过Select方法来实现的,我们进入到他的 selectList 方法,看下他的实现逻辑:

@Overridepublic 
List
selectList(String statement, Object parameter) { return this.sqlSessionProxy.selectList(statement, parameter);}复制代码

我们发现,这个方法内部内部的查询似乎又交给了一层代理,由这一层代理去真正执行的查询操作,我们似乎快找到原因了:

public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,                                 PersistenceExceptionTranslator exceptionTranslator) {    ... 忽略不必要的代码...    this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),        new Class[] { SqlSession.class }, new SqlSessionInterceptor());  }复制代码

果不其然,这个对象在初始化的时候,将这个代理对象也连带着初始化了,这个正是使用的JDK的动态代理来实现的,熟悉动态代理的同学可能会知道,JDK动态代理的精髓也就是 InvocationHandler 的子类,也就是 SqlSessionInterceptor ,我们进入到里面看一下他的实现:

private class SqlSessionInterceptor implements InvocationHandler {    @Override    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {       //获取SqlSession      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)) {          //提交数据          sqlSession.commit(true);        }        //返回查询的数据        return result;      } catch (Throwable t) {        //。。。。忽略不必要代码      } finally {        if (sqlSession != null) {            //关闭SqlSession的连接          closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);        }      }    }  }复制代码

既然SqlSession不一致,那么肯定是在获取SqlSession的时候,里面实现了一些逻辑,从而造成了 SqlSession的不一致,我们进入到 getSqlSession 方法中:

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,      PersistenceExceptionTranslator exceptionTranslator) {   //...忽略不必要代码.... //从ThreadLocal变量里面获取当前的SqlSession的处理器    SqlSessionHolder holder =            (SqlSessionHolder)TransactionSynchronizationManager.getResource(sessionFactory); //如果事务同步管理器处于活动状态则从SqlSessionHolder获取Session    SqlSession session = sessionHolder(executorType, holder);    if (session != null) {      return session;    }    //如果SqlSessionHolder中获取的SqlSession为空,则新建一个SqlSession    session = sessionFactory.openSession(executorType); //若事务同步管理器处于活动状态则将SqlSession设置到SqlSessionHolder中保存起来,以便下次使用    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);    return session;  }复制代码

原来如此,原来并不是说Spring使MyBatis的一级缓存失效了,而是因为Spring只有在开启了事务之后,在同一个事务里的SqlSession会被缓存起来,同一个事务中,多次查询是可以命中缓存的!我们回到 SqlSessionInterceptor#invoke 方法里面,他在关闭的SqlSession的时候同样对 是否开启事务做了处理,我们看 closeSqlSession 方法的源码:

public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) {   //........忽略不必要的代码        SqlSessionHolder holder =           (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);    //查看事务同步管理器是否存在 session     if ((holder != null) && (holder.getSqlSession() == session)) {      holder.released();    } else {      //如果不存在就将该Session关闭掉      session.close();    }  }复制代码

那么,既然导致一级缓存失效的罪魁祸首我们找到了,如何解决呢?

五、解决方案

为什么一级缓存失效,因为两次查询没有使用同一个事物,那么我们加上同一个事物,看看情况如何:

@Test    public void selectTest(){        TestMapper bean = annotationConfigApplicationContext.getBean(TestMapper.class);        //添加事务        DataSourceTransactionManager dataSourceTransactionManager =                annotationConfigApplicationContext.getBean(DataSourceTransactionManager.class);        TransactionStatus transaction =                dataSourceTransactionManager.getTransaction(new DefaultTransactionDefinition());                        List
users = bean.selectUser("周六"); System.out.println(users); List
users1 = bean.selectUser("周六"); System.out.println(users == users1); }复制代码

我们这个时候来看一下结果:

你凭什么说Spring会导致MyBatis的一级缓存失效

 

image-20200707141456766

果然不出我所料,一级缓存又被成功的使用上了。

古人云:耳听为虚,眼见为实!只有真正的经历过,才知道哪些是真,哪些是假!这一次调试源码,不光让我对Spring整合MyBatis有了一个整体的认知,更是让我对动态代理有了一个更加深入的了解,后续我会整理一下,分享出来!

转载地址:http://tqye.baihongyu.com/

你可能感兴趣的文章
nifi使用过程-常见问题-以及入门总结---大数据之Nifi工作笔记0012
查看>>
NIFI分页获取Mysql数据_导入到Hbase中_并可通过phoenix客户端查询_含金量很高的一篇_搞了好久_实际操作05---大数据之Nifi工作笔记0045
查看>>
NIFI分页获取Postgresql数据到Hbase中_实际操作---大数据之Nifi工作笔记0049
查看>>
NIFI同步MySql数据_到SqlServer_错误_驱动程序无法通过使用安全套接字层(SSL)加密与SQL Server_Navicat连接SqlServer---大数据之Nifi工作笔记0047
查看>>
NIFI同步MySql数据源数据_到原始库hbase_同时对数据进行实时分析处理_同步到清洗库_实际操作06---大数据之Nifi工作笔记0046
查看>>
Nifi同步过程中报错create_time字段找不到_实际目标表和源表中没有这个字段---大数据之Nifi工作笔记0066
查看>>
NIFI大数据进阶_FlowFile拓扑_对FlowFile内容和属性的修改删除添加_介绍和描述_以及实际操作---大数据之Nifi工作笔记0023
查看>>
NIFI大数据进阶_FlowFile生成器_GenerateFlowFile处理器_ReplaceText处理器_处理器介绍_处理过程说明---大数据之Nifi工作笔记0019
查看>>
NIFI大数据进阶_Json内容转换为Hive支持的文本格式_操作方法说明_01_EvaluteJsonPath处理器---大数据之Nifi工作笔记0031
查看>>
NIFI大数据进阶_Kafka使用相关说明_实际操作Kafka消费者处理器_来消费kafka数据---大数据之Nifi工作笔记0037
查看>>
NIFI大数据进阶_Kafka使用相关说明_实际操作Kafka生产者---大数据之Nifi工作笔记0036
查看>>
NIFI大数据进阶_NIFI的模板和组的使用-介绍和实际操作_创建组_嵌套组_模板创建下载_导入---大数据之Nifi工作笔记0022
查看>>
NIFI大数据进阶_NIFI监控功能实际操作_Summary查看系统和处理器运行情况_viewDataProvenance查看_---大数据之Nifi工作笔记0026
查看>>
NIFI大数据进阶_NIFI监控的强大功能介绍_处理器面板_进程组面板_summary监控_data_provenance事件源---大数据之Nifi工作笔记0025
查看>>
NIFI大数据进阶_NIFI集群知识点_认识NIFI集群以及集群的组成部分---大数据之Nifi工作笔记0014
查看>>
NIFI大数据进阶_NIFI集群知识点_集群的断开_重连_退役_卸载_总结---大数据之Nifi工作笔记0018
查看>>
NIFI大数据进阶_内嵌ZK模式集群1_搭建过程说明---大数据之Nifi工作笔记0015
查看>>
NIFI大数据进阶_外部ZK模式集群1_实际操作搭建NIFI外部ZK模式集群---大数据之Nifi工作笔记0017
查看>>
NIFI大数据进阶_实时同步MySql的数据到Hive中去_可增量同步_实时监控MySql数据库变化_操作方法说明_01---大数据之Nifi工作笔记0033
查看>>
NIFI大数据进阶_离线同步MySql数据到HDFS_01_实际操作---大数据之Nifi工作笔记0029
查看>>