程序启动时,使用mbp会启用mbp的MybatisMapperRegistry注册mapper。当调用mapper接口时,因为由mbp代理了mapper所以会进入mbp的MybatisMapperProxy代理类,
这里看到根据传入的方法,从methodCache中找寻对应的mybatisMapperMethod类,如果存在返回,不存在则执行后买呢function获取一个新的病放入methodCache中。
然后调用mapperMethod的执行方法。
这很简单,采用命令模式匹配不同的方法,以红框查询返回多条数据举例
先将参数列表进行处理,去除null的参数,返回一个paramMap,它继承于hashMap,简单的重写了一个get方法而已。
这里没有使用RowBounds,直接进入else。这里从mbp传入的sqlsession其实是mybatis-spring的一个sqlsessionTemplate类对象。
这里看到其实sqlsessionTemplate内部还是调用了一个sqlsession代理。
到这里,我们看到有一个getSqlSession。
这里很重要!
TransactionSynchronizationManager是spring的事务管理器,这里使用getResource获取当前sqlsession是否存在事务资源。如果存在则返回sqlsessionholder对象,里面持有一个sqlsession会话。能从sqlsessionholder中获取到当前的sqlsession。如果不存在。getResource返回null,也就无从获取sqlSession了。那么就会进入创建新sqlSession方法。这也就是为什么打开mybatis日志时,sql操作每次都会看到Creating a new SqlSession这句话的原因了。
所以,当我们的方法加入事务时,我们这个方法内部的第一个sql操作进入到这里时从事务管理器获取不到holder,自然进入创建一个新sqlsession,并将sqlsession。sqlsessionfactory注册到spring的事务管理器中。
同一个事务的后续sql操作,再次进入到这里时,就可以从getResource中获取到sqlsessionHolder资源,也就是获取到同一个sqlsession直接返回了。
不过当不存在事务时,一个方法如果有10个查询操作,其实这里时打开10次sqlseesion的。因为没有事务管理器去持有当前的sqlsession资源了。
哦,对了,根据是否可以从事务管理器中获取到sqlSessionHolder资源,在执行方案完毕时,有一个closeSqlSession操作,
其实这里也是根据事务管理器来判断,如果开启了事务,那么当前sqlsession其实不是真正的关闭的。只是对sqlSessionHolder的引用计数-1.
如果没有事务,则是真正的session的关闭操作。
好了,获取sqlsession就到这,下面继续分析获取到sqlsession后面的代码。
这里通过反射执行defaultSqlsession的selectList方法。
这里又涉及到mybatis的概念了。我们其实无论通过mbp的封装还是mapper的操作,其实底层都是mybatis的session和executor。
sqlsession将方法,语句都提供给executor进行执行。
那么这里再分析一波executor,他是哪来的呢。有什么功能呢。
这里其实要从创建sqlSession说起。
我们看到当不存在事务新建一个session时是通过sessionFactory打开一个链接的。这里就先不探讨工厂是哪来的了。只要知道我们这里使用的是default工厂即可。
那么就来到了下面的代码:
当打开一个session时,会传入制定的executor的类型,默认不指定当前会话的事务隔离级别,并且不自动提交事务。
然后看红框中方法
这里就是创建我们sqlsession中使用的executor的地方。可以看到它会根据传入的type不同new不同的实例。这里涉及到一个mybatis的二级缓存。
当我们判断好了我们要使用的executor之后,如果我们cacheEnable==true,也就是开启二级缓存,那么我们这里会使用cachingExecutor来装饰我们的executor。所以这里前面其实是个策略,只不过拿if else直接写了。后者应该是个装饰者模式来开启二级缓存。
简单再说一下cachingExecutor,其实他主要是多了一步缓存操作和一个缓存管理器。
这里可以看到当遇到的是更新操作,他会先清除缓存,当是query操作,他会先获取缓存cache。
好了,executor对象的创建我们说完了。那么继续回到方法的执行。后面会继续分析executor的里操作。
因为mybatis plus 默认开启了二级缓存,所以进入cachingExecutor的query方法
这里看到它先创建一个缓存key。然后调用下面的query方法
进入query,他先从ms中判断是否存在缓存对象。这里最开始有些疑惑的,是因为我们看cache接口。其实这里的逻辑是,如果打开了二级缓存,进入cachingExecutor,那么还需要配置每个mapper开启二级缓存,怎么开启后面说,这里也就是能getCache返回不为null。而是当前mapper制定的缓存实现。
这个应该是从mapper扫描注册时加入的。这里不展开了。知道即可。
回到方法,这里就是如果当前mapper指定了缓存实现,那么就进入到if块中,执行带缓存的逻辑,使用缓存管理器获取缓存,获取到直接返回,否则进入呗装饰的executor执行query方法。如果没有指定mapper的缓存实现,那么直接执行被装饰的executor的query方法。
上面开启sqlsession时根据指定类型new了一个executor,其实他们有一个抽象类。这里可以说是模版模式吧。真正的query方法还是在abstract的baseExecutor中的。
这里重点关注:
这里其实就是mybatis的一级缓存。他也是一个cache的实现类,默认使用永远缓存对象 PerpetualCache,
这里先从一级缓存中获取key对应的value。
如果获取到直接返回list,否则执行queryFromDatabase方法。
进入看到,先在一级缓存中将key存入一个空对象
,然后执行doQuery查询数据。然后进入finally块清除占位。
紧接着再次存入key,value。
然后还判断了一个是否缓存参数。这里不展开了。最后返回list。
那么问题来了,一级缓存默认开启了,那么我们能用吗
其实,当我们使用mybatis plus默认集成时,没啥用。原因接着分析。
那么再简单看一下doQuery吧
其实在baseExecutor中doQuery是个抽象方法。由具体的字类实现。字类就是我们之前根据type执行的被cachingExecutor装饰的executor对象。
以simpleExecutor举例
这里获取到一个statementHandler对象,执行他的query方法。
这里最终进入druid链接池中,最终将sql语句交给Statement发送到db中去执行
然后resultSetHandler.handleResultSets(statement);获取结果。
这时候sql执行完毕了,当我们回到Abstract的BaseExecutor的query方法
执行后,返回list
然后看红框处,这里判断了配置的一级缓存级别。一级缓存有两个级别,一个是mybatis的sqlsession级别;一个是每次发往db的statement级别。
如果一级缓存是statement级别的话,则清除一级缓存。
到这里我们代码的分析完毕了。
那么还剩两个问题:
1.一级缓存起作用了吗
如果我们代码使用的是mybatis的sqlSession来操作或者使用mbp并且开启了事务。那么一个请求使用的都是一个sqlSession,那么一级缓存是生效的。他可以让你在一个sqlSession中对同一个sql进行缓存,只请求一次数据库;但是,集成mbp后,我们没开启事务的情况下,默认是每次sql操作都是新建一个sqlSession,所以一级缓存没有效果。
2,二级缓存怎么用呢
首先如果不使用二级缓存,可以什么都不配置,或者配置:
mybatis-plus:
configuration:
cache-enabled: false
如果使用二级缓存,需要使用在mapper接口上标注
@CacheNamespace
@CacheNamespaceRef
或者在XML文件中配置:
这里注意的是,注解@CacheNamespace和
但是因为我们使用的是mbp。所以有一部分操作是使用的自动ORM,有一部分使用的XML方式。
下面我整理了一个表格:
|
执行xml方法 |
执行mapper接口方法 |
mapper.xml配置cache |
✅ |
❌ |
mapper接口配置cache |
❌ |
✅ |
|
执行xml方法 |
执行mapper接口方法 |
xml配置ref,接口配cachenamespace |
✅ |
✅ |
xml配置cache,接口配cachenamespaceRef |
✅ |
✅ |
推荐配置是在mapper接口上使用@CacheNamespace配置。
在XML文件中使用
下面这组也可以,不过不推荐;个人觉得XML配置主cache配置比较繁琐。
这里扩展一下,我们现在的微服务,都是无状态的多实例部署,那么如果开启了mybatis缓存,可能会造成脏数据问题。所以分布式环境下,建议不开启或者自己实现mybatis的cache接口。实现分布式缓存,比如redis。