mybatis原理分析(二)---深入理解Executor

1.概述

mybatis源码分析(一)中回顾了JDBC的使用和特点,本篇博客将介绍mybatis中一个重要的组件Executor。

可以简单的将mybatis的执行过程分成4个阶段:接口代理、sql会话、执行器、JDBC处理器。各自的作用如下:

  1. 接口代理:是为了简化对Mybatis的使用,底层使用基于接口的动态代理实现。
  2. sql会话:提供了增删改查的基本API,业务逻辑交给执行器处理。
  3. 执行器:处理SQL请求、事务管理、批处理和维护缓存等。决定如何执行sql请求,然后交给JDBC处理器执行具体的sql。
  4. 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个

  1. 相同的sql映射声明,即MappedStatement相同
  2. 必须是连续的sql
  3. 相同的sql语句

看如下的测试代码

  1. 验证相同的MappedStatement

    setName和setName2有相同的sql语句,但是没有相同的MappedStatement。在这里插入图片描述
    在这里插入图片描述

    执行前的数据库如下:

    在这里插入图片描述

    执行之后的控制台输出和数据库结果如下:

    可以看到sql预编译进行了两次,前两次满足条件所以共用一个statement进行批处理。而第三个因为MappedStatement不相同所以无法进行批处理。

    在这里插入图片描述

  2. 验证必须连续

    修改了上面的测试代码,将两个相同的sql和有相同的MappedStatement的代码分割开了在这里插入图片描述

    执行的结果如下

    可以看到进行了3次的预编译,所以验证了必须连续的才可以进行批处理。在这里插入图片描述

  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的特点和实现原理,做个简单的总结。

  1. SimpleExecutor

    每处理一次会话当中的sql请求都会通过StatementHandler构建一个新的statment。

  2. ReuseExecutor

    在同一个会话中第二次执行相同sql会使用之前构建好的statement。就算是statement不一样只要在同一个会话中,sql语句相同即可。

  3. BatchExecutor

    批处理执行器,连续相同的SQL语句并且相同的SQL映射声明会重用statement,执行批处理。

    批处理仅对增删改有效,对查无效。

  4. 底层默认使用的PreparedStatement

4. 后续

关于Executor一级、二级缓存和事务相关的知识,下一篇博客中介绍。

猜你喜欢

转载自blog.csdn.net/gongsenlin341/article/details/108647443
今日推荐