Spring整合MyBatis为什么导致MyBatis一级缓存失效




如题,Spring整合MyBatis为什么导致MyBatis的一级缓存失效?


一、搭建失效场景

测试用例为,查询一个String类型的字符串

基础的环境配置扫描路径配置就不再重复,直接看测试数据相关的代码信息

1、对应的mapper接口及对应的SQL语句(返回一个mobian的字符串)

请添加图片描述



1、使用MyBatis查询同一条SQL

1)直接调用编写好的工具类,开启对应的连接,执行三条一模一样的查询
请添加图片描述



2)查看对应的测试结果
请添加图片描述



熟悉MyBatis的小伙伴都知道MyBatis默认开启一级缓存,当我们执行一条查询语句之后,MyBatis会以我们查询的信息生成一个缓存key,查询的结果为value,存到一个map中,即存入一级缓存,下面是具体的源码,感兴趣可以看看。

缓存key生成的地方
请添加图片描述



每次 查询完会将查询的结果放入缓存池中
请添加图片描述



如果对应的LocalCacheScope是STATEMENT类型,那么就清空缓存池中的缓存(清除MyBatis中的一级缓存)
请添加图片描述



3)关闭一级缓存后再次测试

配置对应的LocalCacheScope类型,配置为STATEMENT类型,即关闭一级缓存

请添加图片描述



测试结果:

我们会发现,此时打印了三次同样的SQL,即每一次查询都是单独的去数据库查询

请添加图片描述





2、使用Spring整合MyBatis查询同一条SQL

1)同样查询三次SQL,直接访问对应的路径即可完成测试
请添加图片描述


2)查看对应的测试结果
请添加图片描述


我们在测试Spring整合MyBatis的用例时,MyBatis的一级缓存使用默认值,即开启一级缓存,但是我们的测试结果却表明,MyBatis的一级缓存好像没起作用。这也就时本文的重点——Spring整合MyBatis后一级缓存失效



3)添加@Transactional注解
请添加图片描述



此时我们的测试结果表明,三条SQL查询又使用了缓存

请添加图片描述






二、原理分析(重点)

结论:Spring将MyBatis的DefaultSqlSession类替换成了SqlSessionTemplate。

MyBatis的一级缓存是基于SqlSession来实现的,对应MyBatis中sqlSession接口的默认实现类是DefaultSqlSession,如果执行的SQL相同时,并且使用的是同一个SqlSession对象,那么就会触发对应的缓存机制。

但是在Spring整合MyBatis后,Spring使用MyBatis不再是直接调用MyBatis中的信息,而是通过调用调用mybatis-spring.jar中的类,继而达到间接调用MyBatis的效果。但在mybatis-spring.jar中,引入了一个SqlSessionTemplate类,它和Spring的事务管理器共同配合,创建对应的SqlSession连接。

即在没有添加@Transactional注解的情况下,每调用一次查询SQL,就会通过SqlSessionTemplate去创建sqlSession,即相当于新创建一次连接,故而每次查询在调试结果看来就是一级缓存失效。

结尾处还有原理总结,请一定要看完!!!



1、明白的其它知识点

本文关于MyBatis中Mapper接口与Spring中Bean相互整合的部分就不再过多重复,感兴趣的可以查看我之前的写的博客。

@Autwired自动注入XxxMapper接口原理(含mybstis-spring.jar源码)

最终得到的结论就是,Spring中使用@Autwired注解,注入xxMapper接口的时候,实际上是注册的是一个代理对象MapperFactoryBean(Spring的Bean是会默认忽略接口的,所以对应的扫描成为BD的逻辑也就是靠mybatis-spring来完成),注册了多少个mapper接口,就会生成多少个MapperFactoryBean代理对象,只是每个代理对象的mapperInterface不同罢了。

请添加图片描述



想要明白为什么可以通过该方式实现调用不同类型的Mapper代理出来的Bean的原理,需要你了解访问实现了FactoryBean接口的Bean的访问流程,巧了,你又有地方可以参考了:

浅谈使用实现FactoryBean接口的方式定义Bean



2、调用入口是接口中getObject方法

1)

为什么这个方法是入口,也请参考上面的博文浅谈使用实现FactoryBean接口的方式定义Bean

获取到对应的sqlSession,然后getMapper。如果你记得只MyBatis查询sql的时候你就会发现这两者的长相非常相似,区别也就仅仅存在于getSession现在是一个方法,而MyBatis是一个对象

请添加图片描述



2)

点开方法会会发现,该方法也就是一个sqlSession。顺着该对象的实例化流程继续往下看,即进如SqlSessionTemplate类中

请添加图片描述



3)

最后会进入到这个位置,即在实例化sqlSessionTemplate对象时,会实例化这些属性,查看这些属性的类型就会发现,sqlSessionProxy就是我们最终需要的sqlSession对象

请添加图片描述



4)

继续跟进对应的SqlSessionInterceptor代理逻辑部分代码,会发现sqlSession是调用getSqlSession方法生成,继续跟进

请添加图片描述



5)

最终到达该方法

该方法的大致上分为四部分:

  1. 去Sping的事务管理器中获取对应的SqlSessionHolder对象(包装了一层的sqlSession对象,可以理解为就是sqlSession对象)
  2. 如果不为空,就直接返回(缓存中有对应的sqlSession对象)
  3. 如果没有就开启对应的连接,这里是直接调用MyBatis中的openSession方法
    请添加图片描述
  4. 然后将对应的sqlSession对象注册到缓存中(第二步就是往这个缓存里面获取)

请添加图片描述




3、为什么加了@Transactional注解就可以使用缓存

6)

核心就是注册的方法,我测试的场景是没有加@Transactional注解的时候,此处判断为false就不会再向缓存中添加数据。

当然如果判断成功就是会调用TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory))方法,将该sqlSession对象添加到对应的缓存中,数量+1

请添加图片描述
即最终注册到synchronizations对象的缓存中

请添加图片描述



7)

缓存池使用的是一个ThreadLocal(用于处理多个线程中数据的隔离问题,内部维护一个ThreadLocalMap)来存储

synchronizations = new NamedThreadLocal<>(“Transaction synchronizations”);

请添加图片描述

我认为,在MyBatis中使用sqlSession对象进行数据查询时,当遇到并发量比较大的一些情况时,就有可能出现线程不安全的情况,换句话说就是事务之间的隔离性没有做好。

该类是Spring中的事务同步管理器类,synchronizations集合用于存放每一个线程的事务同步器,即存放一个线程的所有事务,使用ThreadLocal类型来存储,可以保证不同线程之间的数据安全性。




4、再总结

如果我们没有添加@Transactional注解,Spring认为我的每一次查询都都是相互独立的,便开启了三次不同的事务也即是创建了三个不同的sqlSession对象。即无法使用到MyBatis的一级缓存。



如果我们添加了@Transactional注解,Spring在执行了第一次查询后,会将当前线程的事务情况存储到synchronizations 的集合中,当第二次再执行查询的时候,能够在缓存中直接获取到当前的事务情况(包含sqlSession对象),即不会再去调用openSession方法,继而创建一个新的sqlSession对象,而是使用缓存中的sqlSession对象(对应的代码在上面的第五步)。这就保证了在添加@Transactional注解的情况下,能够走MyBatis的一级缓存

请添加图片描述

猜你喜欢

转载自blog.csdn.net/qq_44377709/article/details/120578173