Mybatis中的执行器(Executor)

在之前的文章中介绍了mybatis的基本使用,从使用流程中可知,在每次执行CURD的时候,都需要获取SqlSession这个对象,接口如下:

可以看出来这个接口主要定义类关于CRUD、数据库事务、数据库刷新等相关操作。下面看它的默认实现类:

可以看到 DefaultSqlSession 实现了SqlSession中的方法,(其实我们自己也可根据需要去实现)

而在这个方法中,存在一个属性就是今天需要将的主角,Mybatis的执行器(Executor)。

Executor简单介绍

Executor执行器,是mybatis中执行查询的主要代码,Executor分为三种,分别是简单执行器SimpleExecutor、可重用执行器ReuseExecutor、批量执行器BatchExecutor。可以在mybatis的配置文件中设置使用哪种执行器:

 源码中,初始化SqlSession的时候,会查看配置文件中是否有配置,没有则使用SimpleExecutor

 下面分别简单使用三个执行执行以下代码:

首先初始化以下相关需要的信息:

1、SimpleExecutor

执行结果:

当执行两次查询的时候,可以看到每次执行都会进行一次预编译(就是创建一个PrepareStatement),这样效率相对比较低。

2、 ReuseExecutor

执行结果:

可以看到,当使用ReuseExecutor的时候,虽然执行了2次查询,但是只执行了一次预编译。相对简单执行,可重用执行器的效率会高一些。

3、BatchExecutor

批量执行器,当查询的时候,跟SimpleExecutor一样,也会执行多次的预编译。而更新或插入操作的时候,会批量进行,但是要注意需要手动进行提交:

下面我们看一下执行器的类图:

 通过类型可以发现,在第二层中,存在两个实现类分别是 BaseExecutor和CachingExecutor两个类。其实他们两个分别是实现mybatis中一级缓存和二级缓存的。

一级缓存代码

跟踪BaseExecutor可以看到,在query方法中,实现了缓存逻辑,当缓存不存在的时候,则调用实现类中的doQuery。

 创建一级缓存的KEY:

 查看缓存中是否存在,存在则直接返回,不存在则查询数据库:

查询数据库:

这就是Mybatis中的以及缓存,逻辑十分简单,可以从源码中看到,一级缓存是默认开启的并且在同一个线程中有效,同时查询完毕之后,立即建立缓存。

一级缓存的命中场景

mybatis中的一级缓存是存放在hashMap中的,之所以没有用线程安全的集合,主要是因为SqlSession都是线程不安全的,所以没有必须要。

 1、运行时命中相关

① 同一个会话中(所以也被成为会话级缓存)

② Sql语句、参数相同

③ 相同的statementId(相同Sql和参数,在不同的statementId中也不行)

④ RowBounds相同(mybatis分页使用,可以理解为参数相同)

2、操作和配置相关

① 未手动清空缓存(session1.clearCache();)

② 未配置 @Options(flushCache = Options.FlushCachePolicy.TRUE),其实原理同上(在实验中发现必须要跟@Select标签一起使用才能生效)

③ 未执行 Update 语句(在源码中可以看到,只要更新操作就会调用 clearCache()方法)

④ 作用域为STATEMENT(在setting中配置 <setting name="localCacheScope" value="STATEMENT"/>,主要是减小了缓存的作用域,在子查询中还是会使用缓存的)

⑤ 使用事务提交、回滚操作

总结:

1、会话相关

2、参数条件相关

3、提交、事务、更新会清空

一级缓存失效场景

在spring和mybaits中,会出现mybatis一级缓存失效的情况,主要原因是spring的封装,导致每次查询都会创建一个SqlSession,因为一级缓存是会话级缓存,所以导致失效。通过分析源码可知,在同一个事务情况下,spring 不会重新创建SqlSession,所以开启事务即可解决。

spring调用mybatis流程如下:

这里可以看到,在SqlSessionTemplate中是使用sqlSession代理进行调用的

继续追踪下去可以看出来,sqlSessionProxy代理是使用 动态代理 SqlSessionInterceptor 创建的,

 下面查看 SqlSessionInterceptor 中获取SqlSession方法:

到这里获取到的SqlSession,就是执行sql的SqlSession

可以看到他其实从ThreadLocal(也就是一个事务中保存的SqlSession)变量中先获取了一下,是否存在 ,存在直接使用,不存在就重新创建。这也就是为什么开启事务就能命中一级缓存的主要原因。

 可以看到sqlSession被spring中的 SqlSessionTemplate替换了,但是 SqlSessionTemplate 本身没有执行Sql的能力,在其中又采用动态代理获取了DefaultSqlSession。

二级缓存

mybatis中,实现二级缓存是使用CachingExecutor这个类实现的,同时采用装饰者模式,对查询进行调用,源码如下:

在CachingExecutor中,存在一个属性就是 Executor,而CachingExecutor本身只实现缓存相关的内容:

上面的源码可以看到,当缓存开启的时候,先查询缓存中是否存在,不存在则使用装饰着(Executor)进行查询。

下面查看二级缓存怎么启动:

1、在setting中配置缓存开启:

2、在需要开启缓存的Mapper.xml文件中添加如下一行:

编写例子:

虽然使用的 CachingExecutor 但是发现日志输出的缓存命中率是0,这是因为二级缓存需要提交之后才能建立缓存,打开cachingExecutor.commit(ture)

 

在平时开发的过程中,其实直接使用SqlSession进行sql的执行的,并不需要上面那么复杂的代码。

其他配置内容:

 PS:

① flushCache  清空之后,需要提交才能生效,并且只要提交了,所有的缓存都会被清空。

② <cache/> 和 @CacheNamespace 不是一个缓存空间,也就是使用@Select 期望命中缓存,则需要使用 @CacheNamespace,而xml中期望命中缓存,则需要使用<cache/>,这里是需要注意的。

 

跟踪代码可知, 

 当开启缓存的时候,可以看到是先执行二级缓存,再执行一级缓存的。

二级缓存存在的意义

1、二级缓存是跨线程的

2、一级缓存的作用范围相对小,二级缓存的生命周期为整个应用

二级缓存的需求

1、存储的多元性(Redis、Mysql、内存等)

2、存储容量需要限定,就存在淘汰规则(FIFO[先进先出]、LRU[最近最少使用])

3、数据的过期清理

4、保证线程安全

5、缓存的命中率统计

6、序列化

二级缓存组件的结构:

 mybatis二级缓存逻辑实现采用:装饰器+责任链的方式进行实现的。

其实也就是每个上述的需求点,都会对应一个集成Cache接口的类,并且这些类串联起来,最终实现缓存的逻辑。

 debug可以看到如下:

二级缓存命中条件: 

① 必须提交之后(就算sqlsession是自动提交的,也需要手动提交才能生效)

② Sql语句、参数相同

③ statementId 相同

④ 分页条件相同

为什么提交之后才能命中缓存?

主要是因为,二级缓存是跨线程访问的,为了防止其他线程的脏读。

 每个会话都有自己的事务缓存管理,当会话读取的数据的时候,会暂时存放到事务管理器的暂存区中,只有commit之后,才提交到缓存区中,这个时候才能被其他的会话共享。不论是查询还是更新都是暂时放在暂存区中。

其中暂存区的多少取决于使用了多少个命名空间。

执行流程:

 下面的源码可以看到,采用一个标志位clearOnCommit,进行维护,主要是因为,如果另一个会话对缓存的数据进行了update,但是没有commit的时候,update的数据值存在暂存区中。

以上是对mybatis的执行器和缓存机制的简单描述。

这里推荐mybatis讲解非常棒的福利:B站 传送门 真正的干活满满!~

同时盗用一下UP主的图片:

  • 11
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值