文章目录
1.概述
《mybatis源码分析(一)—JDBC》中回顾了JDBC的使用和特点,本篇博客将介绍mybatis中一个重要的组件Executor。
可以简单的将mybatis的执行过程分成4个阶段:接口代理、sql会话、执行器、JDBC处理器。各自的作用如下:
- 接口代理:是为了简化对Mybatis的使用,底层使用基于接口的动态代理实现。
- sql会话:提供了增删改查的基本API,业务逻辑交给执行器处理。
- 执行器:处理SQL请求、事务管理、批处理和维护缓存等。决定如何执行sql请求,然后交给JDBC处理器执行具体的sql。
- JDBC处理器:上篇博客中说明了JDBC用于处理和执行sql语句。在会话中每调用一次增删改查,都会生成一个实例与之对应,除非命中缓存。
在一次会话中,这四个组件的实例比例是1:1:1:n
并且这些组件都不是线程安全的,不能跨线程使用。
当一个SQL请求通过会话到达执行器后,然后交给对应的JDBC处理器进行处理。
2.Executor相关概念
Executor是Mybatis执行者接口,他包含的功能有:
- 基本功能:改、查,没有增删是因为所有的增删操作都可以归结为改。
- 缓存维护:包括创建缓存Key、清理缓存、判断缓存是否存在。
- 事务管理:提交、回滚、关闭、批处理刷新。
Executor有6个实现类,这里先介绍三个重要的实现子类。分别是:SimpleExecutor(简单执行器)、ReuseExecutor(重用执行器)、BatchExecutor(批处理执行器)。
2.1 SimpleExecutor
是mybatis默认的执行器,它每处理一次会话当中的sql请求都会通过StatementHandler构建一个新的statment。例如下面的例子:
@Before
public void init() {
// 1.获取构建器
SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
// 2.获取配置文件的流信息
InputStream resourceAsStream = ExecutorTest.class.getResourceAsStream("/mybatis-config.xml");
// 3.解析XML 并构造会话工厂
sqlSessionFactory = factoryBuilder.build(resourceAsStream);
// 4.获取工厂配置
configuration = sqlSessionFactory.getConfiguration();
// 5.构建jdbc事务
jdbcTransaction = new JdbcTransaction(sqlSessionFactory.openSession().getConnection());
// 6.获取Mapper映射
mappedStatement = configuration.getMappedStatement("com.gongsenlin.executor.dao.UserMapper.selectByid");
}
@Test//简单执行器
public void simpleTest() throws SQLException {
SimpleExecutor simpleExecutor = new SimpleExecutor(configuration, jdbcTransaction);
// 就算是两个一样的sql语句,但每次执行都会进行编译
List<Object> list = simpleExecutor.doQuery(mappedStatement, 10, RowBounds.DEFAULT, SimpleExecutor.NO_RESULT_HANDLER, mappedStatement.getBoundSql(10));
simpleExecutor.doQuery(mappedStatement, 10, RowBounds.DEFAULT, SimpleExecutor.NO_RESULT_HANDLER, mappedStatement.getBoundSql(10));
System.out.println(list.get(0));
}
simpleTest执行的结果如下,可以看到相同的sql语句,每执行一次都会编译一次。
接下来点进源码中看看。看下doQuery是如何实现的。首先获取配置信息,根据配置信息构建一个StatementHandler。然后调用prepareStatement来预编译sql,构建新的statement。
跟着源码再看看这做了些什么。主要就是构建一个statement 然后给statementHandler设置参数。
点进prepare方法,底层是使用上篇博客中jdbc中预编译sql的代码。connection.prepareStatement()来得到statement,然后设置超时间和数据库返回行数。
而在parameterize方法中,可以看到这里将Statement强制转成了PreparedStatement。所以默认是使用的PreparedStatement来执行sql。这也是比较安全的,可以防止sql注入。
构建好了handler就会执行handler的query方法。这里就先不细看handler是如何工作的了,之后再另写一篇博客来详细的介绍StatementHandler。
综上的源码分析,可以验证之前得出的结论,每处理一次会话当中的sql请求都会通过StatementHandler构建一个新的statment。
2.2 ReuseExecutor
看名字就知道这是一个重用执行器,那么重用的是什么东西呢?
我们将上面例子中的简单执行器换成重用执行器,再执行一次看看有什么区别。统一是执行两个一样的sql语句。结果如下
可以发现这里只预编译了一次sql。也就是说在同一个会话中第二次执行相同sql会使用之前构建好的statement。
让我们来看看源码是如何实现的。debug调试进入doQuery方法。
结构上和SimpleExecutor没什么区别。那么来看看里面的方法有什么差别。
下面是ReuseExecutor中的prepareStatement,它是如何获得一个statement的呢?
这里比简单执行器多了一步判断当前的sql语句是否在缓存中出现了,并且是在同一个会话下。若有则从缓存中获取对应的statement不用再预编译sql来获得statement。没有的话,则和简单执行器一样的方式构建。然后放入statementMap缓存中。sql语句作为key,statement作为value。
之后的逻辑就和简单执行器一样了。从源码中也可以看出这样做的效率会高一点.
综上ReuseExecutor 区别在于他会将在会话期间内的Statement进行缓存(Map<String, Statement> statementMap),并使用SQL语句作为Key。所以当执行下一请求的时候,不在重复构建Statement,而是从缓存中取出并设置参数,然后执行。
就算是两个不同的方法,对应的两个MapperedStatement不一样,但是sql语句一样的话,不在重复构建Statement而是使用同一个jdbc中的statement。这也说明了为什么不能跨线程使用,因为多个线程可能会给同一个statement设置参数。
2.4 BatchExecutor
BatchExecutor 顾名思议,它就是用来作批处理的。但会将所有SQL请求集中起来,最后调用Executor.flushStatements() 方法时一次性将所有请求发送至数据库。
这里它是利用了Statement中的addBath机制吗?
不一定,因为只有连续相同的SQL语句并且相同的SQL映射声明,才会重用Statement,并利用其批处理功能。否则会构建一个新的Satement然后在flushStatements() 时一起执行。这么做的原因是它要保证执行顺序。跟调用顺序一至。
能进行批处理的条件有3个
- 相同的sql映射声明,即MappedStatement相同
- 必须是连续的sql
- 相同的sql语句
看如下的测试代码
-
验证相同的MappedStatement
setName和setName2有相同的sql语句,但是没有相同的MappedStatement。
执行前的数据库如下:
执行之后的控制台输出和数据库结果如下:
可以看到sql预编译进行了两次,前两次满足条件所以共用一个statement进行批处理。而第三个因为MappedStatement不相同所以无法进行批处理。
-
验证必须连续
修改了上面的测试代码,将两个相同的sql和有相同的MappedStatement的代码分割开了
执行的结果如下
可以看到进行了3次的预编译,所以验证了必须连续的才可以进行批处理。
-
严重sql必须相同
测试代码如下:
setName和addUser执行不同的sql语句。这也是最好理解的必然是无法批处理的。
结果如下
2.4.1 批处理的效率
分别使用批处理执行器和重用执行器去执行添加100个新用户,记录时间,代码如下
批处理用时326毫秒
对照实验
多次单条执行用时588毫秒
可以看出批处理的效率更高。
2.4.2 批处理查询
批处理提高效率仅对增删改有效果,对查询没效果。将刚才的两组对照实验修改for循环中的addUser方法改成mapper.selectByid(10);
执行的结果如下,几乎没有差别。
2.4.3 源码实现
编写如下测试代码debug调试来看看源码是如何实现的批处理
首先setName会执行到BatchExecutor中的doUpdate方法,在这里打一断点。
这里有一个if判断,就是判断能否批处理的三个条件。
currentSql和currentStatement记录的是上一条sql的信息。
而现在是第一次进来所以这两个变量都是null。必定是走else的逻辑。
else的逻辑会构建一个新的statament 然后并记录下来现在的sql和statement。并将statement添加到statement队尾,添加一个批处理结果集到结果集队尾。
然后执行handler的batch
而这里就是使用的jdbc的addBatch。第二条addUser代码 也会走else的逻辑。
第三条addUser,因为满足批处理的三个条件那么会走if的逻辑。
if的逻辑中直接从statement队列中拿出队尾的statement,和结果集队列中的队尾的BatchResult。设置参数即可。
执行完所有的5次doUpdate方法后,有三个statement和三个batchResult
执行flushStatements进行批处理。真正的执行逻辑在BatchExecutor中的doFlushStatements,依次的拿出statement,执行批处理。
3. 总结
详细介绍了三种Executor的特点和实现原理,做个简单的总结。
-
SimpleExecutor
每处理一次会话当中的sql请求都会通过StatementHandler构建一个新的statment。
-
ReuseExecutor
在同一个会话中第二次执行相同sql会使用之前构建好的statement。就算是statement不一样只要在同一个会话中,sql语句相同即可。
-
BatchExecutor
批处理执行器,连续相同的SQL语句并且相同的SQL映射声明会重用statement,执行批处理。
批处理仅对增删改有效,对查无效。
-
底层默认使用的PreparedStatement
4. 后续
关于Executor一级、二级缓存和事务相关的知识,下一篇博客中介绍。