从前面分析我们知道了sql的具体执行是通过调用SqlSession接口的对应的方法去执行的,而SqlSession最终都是通过调用了自己的Executor对象的query和update去执行的。本文就分析下sql的执行器-----Executor。
1、Executor继承体系
下图展示的是MyBatis的执行器的核心类的继承体系图。
Executor是执行器的顶层接口,它定义了查询、更新事务提交、回滚等一系类和执行SQL有关的方法。他有两个直接实现类:BaseExecutor和CachingExecutor
BaseExecutor:是一个抽象类,这种通过抽象的实现接口的方式是适配器设计模式的体现,是 Executor 的默认实现,实现了大部分 Executor 接口定义的功能,降低了接口实现的难度。BaseExecutor有三个子类,分别对应三种执行器类型,在ExecutorType这个枚举类中有定义,如下图所示。
- SIMPLE对应SimpleExecutor,是一种常规执行器,每次执行都会创建一个statement,用完后关闭,默认值。
- REUSE对应ReuseExecutor,它是一个可重用Statement对象的执行器,不会关闭statement,而是把statement放到缓存中(Map中)。缓存的key为sql语句,value即为对应的statement。也就是说不会每一次调用都去创建一个 Statement 对象,而是会重复利用以前创建好的(如果SQL相同的话),这也就是在很多数据连接池库中常见的 PSCache 概念 。
- BATCH对应BatchExecutor,一个用于执行存储过程的和批量操作的执行器,他也会重用Statement 对象。
在MyBatis的三种执行器,我们可以在配置文件中通过设定settings
中的defaultExecutorType
属性的值来选择,通常来说:
SimpleExecutor 比 ReuseExecutor 的性能要差 , 因为 SimpleExecutor 没有做 PSCache。为什么做了 PSCache 性能就会高呢 , 因为当SQL越复杂占位符越多的时候预编译的时间也就越长,创建一个PreparedStatement对象的时间也就越长。猜想中BatchExecutor比ReuseExecutor功能强大性能高,实际上并非如此,BatchExecutor是没有做PSCache的。BatchExecutor 与 SimpleExecutor 和 ReuseExecutor 还有一个区别就是 , BatchExecutor 的事务是没法自动提交的。因为 BatchExecutor 只有在调用了 SqlSession 的 commit 方法的时候,它才会去执行 executeBatch 方法。
CachingExecutor:缓存执行器,MyBatis二级缓存的实现依赖他实现,先从缓存中查询结果,如果存在,就返回结果;如果不存在,再委托给 Executor delegate 去数据库中取,delegate 可以是上面任何一个执行器,默认是SimpleExecutor。
2、Executor创建过程工作原理
上面说到过,在配置文件中可以通过设定settings
中的defaultExecutorType
属性的值来选择执行器类型,如果没有设置,则默认会选择SimpleExecutor。其实创建Executor只是创建SqlSession的一个子步骤,在我的另一篇博文:MyBatis源码学习—SqlSession的运行过程中有过分析,这里再来分析一下。下图所示就是创建SqlSession的核心代码,在try块倒数2行就是创建执行器的逻辑,我们看到它调用了Configuration对象的newExecutor
方法
追踪到newExecutor方法内部看看执行器创建具体创建流程,如下图所示。
上面代码的逻辑大致如下:
- 根据形参
executorType
是否null获得即将要创建的执行器的类型,如果executorType==null,那就创建SimpleExecutor,否则创建指定类型的执行器; - 根据执行器类型
executorType
直接new指定类型的执行器; - 判断是否配置开启了二级缓存(cacheEnable==true?),如果开启了的话,那就在new一个CachingExecutor包装一下上面的简单执行器executor;
在创建Executor成功之后,MyBatis会执行下面一行代码:
Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
这里用到了责任链模式,主要就是MyBatis配置插件,这里它将为我们构建一层层的动态代理对象。在调度真实的Executor方法之前执行配置插件的代码可以修改。
到这里,执行器Executor实例就被创建出来了,伴随着SqlSession对象实例也即将创建成功。接下来我们分析一个Executor中主要方法的执行流程。
3、Executor接口的主要方法
Executor接口的方法还是比较多的,这里我们就几个主要的方法和调用流程来做一个简单的描述。Executor中的大部分方法的调用链其实是差不多的,下面都是深入源码分析执行过程,下图所示是MyBatis的一般执行过程。
query()方法
query方法有两种形式,一种是直接查询;一种是从缓存中查询,下面来看一下源码
当有一个查询请求访问的时候,首先会经过Executor的实现类CachingExecutor,先从缓存中查询SQL是否是第一次执行,如果是第一次执行的话,那么就直接执行SQL语句,并创建缓存,如果第二次访问相同的SQL语句的话,那么就会直接从缓存中提取
CachingExecutor类中对query()方法实现
首先会由第一个query()方法执行,他会创建缓存的key以及从MappedStetament中获取到SQL基本信息,然后调用第二个query()方法,这个方法中首先会从缓存中查询,没有数据调用delegate执行器(BaseExecutor下的三个子执行器实例)的query()方法。
SimpleExecutor的doQuery()方法
经过一系列的调用,最终来到了实例类的doQuery()方法,这里首先以SimpleExecutor为例分析,如下图所示。
显然MyBatis根据Configuration对象构建了一个StatementHandler对象,然后使用prepareStatement方法,对SQL编译并对参数进行初始化,我们在看它的实现过程,它调用了StatementHandler的prepare()进行了预编译和基础设置,然后通过StatementHandler的parameterize(来设置参数并执行,resultHandler 再组装查询结果返回给调用者来完成一次查询。
ReuseExecutor的doQuery()方法
几乎和SimpleExecutor完成的工作一样,其内部不过是使用一个Map来存储每次执行的查询语句,为后面的SQL重用作准备(Map中存缓存的其实是Statement)。
上面的源码可以看出,Executor执行器所起的作用相当于是管理StatementHandler 的整个生命周期的工作,包括创建、初始化、解析、关闭。
BatchExecutor的doQuery()方法
工作原理和SimpleExecutor基本类似,不过要注意的是BatchExecutor支持一次性将多条SQL语句送到数据库执行。
update()方法
还是一样,在开启缓存的情况下会首先执行CachingExecutor的的update()方法,如下图所示。
首先还是会调用到BaseExcutor的doUpdate()方法,之后调用到具体实例子类执行器的doUpdate()
SimpleExecutor的doUpdate()方法
逻辑大致和query()方法的逻辑类似,图中有注释这里不再解释。
queryCursor()方法
我们查阅其源码的时候,在执行器的执行过程中并没有发现其与query方法有任何不同之处,但是在doQueryCursor() 方法中我们可以看到它返回了一个cursor对象,网上搜索cursor的相关资料并查阅其基本结构,得出来的结论是:用于逐条读取SQL语句,应对数据量
// 查询可以返回Cursor<T>类型的数据,类似于JDBC里的ResultSet类,
// 当查询百万级的数据的时候,使用游标可以节省内存的消耗,不需要一次性取出所有数据,可以进行逐条处理或逐条取出部分批量处理。
public interface Cursor<T> extends Closeable, Iterable<T> {
boolean isOpen();
boolean isConsumed();
int getCurrentIndex();
}
flushStatements() 方法
flushStatement()的主要执行流程和query,update 的执行流程差不多,flushStatement()的主要作用,flushStatement()主要用来释放statement,或者用于ReuseExecutor和BatchExecutor来刷新缓存
在 SimpleExecutor中它就是返回了一个空集合对象
在 ReuseExecutor中它用于释放缓存的Statement,同样也是返回了空集合
createCacheKey() 方法
createCacheKey()方法用于创建缓存数据的key的,要想了解这个方法的原理,我们需要先研究一下CacheKey这个类,下图是Cachekey的属性和构造方法
在cache中唯一确定一个缓存项需要使用缓存项的key,Mybatis中因为涉及到动态SQL等多方面因素,其缓存项的key不能仅仅通过一个String表示,所以MyBatis 提供了CacheKey类来表示缓存项的key,在一个CacheKey对象中可以封装多个影响缓存项的因素。
key的hashCode
17是质子数中一个“不大不小”的存在,如果你使用的是一个如2这样较小的质数, 那么得出的乘积会在一个很小的范围,很容易造成哈希值的冲突。 而如果选择一个100以上的质数,得出的哈希值会超出int的最大范围,这两种都不合适。 而如果对超过 50,000 个英文单词(由两个不同版本的 Unix 字典合并而成)进行 hash code 运算, 并使用常数 31, 33, 37, 39 和 41 作为乘子(cachekey使用37),每个常数算出的哈希值冲突数都小于7个(国外大神做的测试), 那么这几个数就被作为生成hashCode值得备选乘数了
最终一个CacheKey中的key的hashcode=乘子{31,33,37,39或41}*oldHashCode+objectHashCode
Executor 的现实抽象
在上面的分析过程中我们了解到,Executor执行器是MyBatis中很重要的一个组件,Executor相当于是外包的boss,它定义了甲方(SQL)需要干的活(Executor的主要方法),这个外包公司是个小公司,没多少人,每个人都需要干很多工作,boss接到开发任务的话,一般都找项目经理(CachingExecutor),项目经理几乎不懂技术,它主要和技术leader(BaseExecutor)打交道,技术leader主要负责框架的搭建,具体的工作都会交给下面的程序员来做,程序员的技术也有优劣,高级程序员(BatchExecutor)、中级程序员(ReuseExecutor)、初级程序员(SimpleExecutor),它们干的活也不一样。一般有新的项目需求传达到项目经理这里,项目经理先判断自己手里有没有现成的类库或者项目直接套用(Cache),有的话就直接让技术leader拿来直接套用就好,没有的话就需要搭建框架,再把框架存入本地类库中,再进行解析。
参考资料
- [1] https://www.cnblogs.com/cxuanBlog/p/11178489.html#executor%E6%8E%A5%E5%8F%A3%E7%9A%84%E4%B8%BB%E8%A6%81%E6%96%B9%E6%B3%95
- [2] https://blog.csdn.net/xl3379307903/article/details/80517841