mybatis之执行器

本篇来聊一下mybatis的执行器,看看如何在不同场景使用不同执行器以及不同执行器的实现原理是怎样的(基于mybatis 3.4.6)。
知识点

  • 什么是执行器
  • mybatis执行器类型及何时使用
  • 各个执行器的实现原理
    什么是执行器
    顾名思义,执行器就是用来执行我们的 sql 语句,从而取到结果或对数据库进行更新的东西。它是 mybatis 中非常核心的概念,它提供了增、改、查、事务管理等基本操作接口,基本上所有的东西都是围绕执行器来进行,看下图
    mybatis之执行器
    文章图片

    mybatis执行器类型及何时使用
    执行器类型我们先来看下 mybatis 目前有哪些执行器类型
  • SIMPLE
  • REUSE
  • BATCH
    我们能配置的目前就以上三种执行器,通过 defaultExecutorType(注意是全局的)来指定使用哪种,如果是通过 xml 方式配置的,则参照官网
    mybatis之执行器
    文章图片

    如果是使用spring boot集成的,则如下
    mybatis之执行器
    文章图片

    下面我们分别对三种执行器来一一介绍。
    SIMPLE可以理解为基本的执行器,这也是 mybatis 的默认执行器,也是我们平时用的最多的执行器,我们无需做任何配置更改。它大致的流程是:打开连接-> 设置 Statement -> 参数注入 -> 执行 -> 结果映射 -> 关闭 Statement ,可以看到每次用完之后会把 Statement 关掉。
    REUSE看名字就知道这是一个可重用执行器,什么叫可重用呢?指的是 simple 流程中的前面两步可以重复利用,也就是 打开连接-> 设置 Statement,这两步会创建一个新的 Statement,reuse 执行器内部维护一个缓存,第一次获取后就会以对应的 sql (占位符 ? 不被具体参数替换)作为key进行缓存该 Statement,并且不对 Statement 进行关闭,后面遇到该 sql 只要从第三步开始做就可以了,这样就带来了性能上的优化(实际上优化并不大)。
    BATCH同样通过名称就能看出来这是一个批量执行器,批量是什么意思?就是说它是可以一次性执行一批 sql 语句的,主要是针对更新、插入等修改性操作,对于单条或者查询类操作,就不要用这个执行器了,为什么能做到批量执行呢,其实本质上是用的 sql 包里的 PreparedStatementaddBatch(),后面原理部分再细说。
    何时使用清楚了三种执行器类型的特点之后,我们再来对比下在不同场景下的性能输出,这样大家就清楚何时使用何种类型执行器了,这里没有对比更新操作,是因为插入操作本质上用的就是更新操作接口。基于 spring 使用的 mybatis,基于以下表结构
    mybatis之执行器
    文章图片

    单条查询先上代码
    DefaultSqlSessionFactory sqlSessionFactory = (DefaultSqlSessionFactory)applicationContext.getBean("sqlSessionFactory"); //这里自己选择执行器类型 DefaultSqlSession defaultSqlSession = (DefaultSqlSession)sqlSessionFactory.openSession(ExecutorType.SIMPLE); long startTime = System.currentTimeMillis(); //userInfoMapper.selectById(22222); defaultSqlSession.selectList("com.example.mybatisanalyze.mapper.UserInfoMapper.selectById", 22222); long endTime = System.currentTimeMillis(); System.out.println((endTime - startTime) + "ms");

    simple:
    mybatis之执行器
    文章图片

    从图中可以看出花了42ms
    reuse:
    DefaultSqlSession defaultSqlSession = (DefaultSqlSession)sqlSessionFactory.openSession(ExecutorType.SIMPLE); 改为 DefaultSqlSession defaultSqlSession = (DefaultSqlSession)sqlSessionFactory.openSession(ExecutorType.REUSE);
    mybatis之执行器
    文章图片

    从图中可以看出花了34ms
    batch:
    DefaultSqlSession defaultSqlSession = (DefaultSqlSession)sqlSessionFactory.openSession(ExecutorType.SIMPLE); 改为 DefaultSqlSession defaultSqlSession = (DefaultSqlSession)sqlSessionFactory.openSession(ExecutorType.BATCH);
    mybatis之执行器
    文章图片

    从图中可以看出花了43ms
    结论:单条查询三种执行器差不多,但是建议选择simple,原因见执行器类型中说明。
    单条插入先上代码
    DefaultSqlSessionFactory sqlSessionFactory = (DefaultSqlSessionFactory)applicationContext.getBean("sqlSessionFactory"); //执行器类型自己选择 DefaultSqlSession defaultSqlSession = (DefaultSqlSession)sqlSessionFactory.openSession(ExecutorType.SIMPLE); long startTime = System.currentTimeMillis(); UserInfo userInfo = new UserInfo(); userInfo.setNickName("bbbcd"); userInfo.setUserName("abcdc"); userInfo.setBirthday(new Date()); userInfo.setRegisterTime(LocalDateTime.now()); userInfo.setEmail("12345"); defaultSqlSession.insert("com.example.mybatisanalyze.mapper.UserInfoMapper.insert", userInfo); long endTime = System.currentTimeMillis(); System.out.println((endTime - startTime) + "ms");

    simple:
    mybatis之执行器
    文章图片

    从图中可以看出花了172ms
    reuse:
    代码中DefaultSqlSession defaultSqlSession = (DefaultSqlSession)sqlSessionFactory.openSession(ExecutorType.SIMPLE)这行改为DefaultSqlSession defaultSqlSession = (DefaultSqlSession)sqlSessionFactory.openSession(ExecutorType.REUSE);
    mybatis之执行器
    文章图片

    从图中可以看出花了178ms
    batch:
    代码中DefaultSqlSession defaultSqlSession = (DefaultSqlSession)sqlSessionFactory.openSession(ExecutorType.SIMPLE)这行改为DefaultSqlSession defaultSqlSession = (DefaultSqlSession)sqlSessionFactory.openSession(ExecutorType.BATCH);
    mybatis之执行器
    文章图片

    从图中可以看出花了176ms
    结论:三种执行器性能差不多,建议选择simple
    批量插入先上代码
    DefaultSqlSessionFactory sqlSessionFactory = (DefaultSqlSessionFactory)applicationContext.getBean("sqlSessionFactory"); //这里执行器类型自己选择 DefaultSqlSession defaultSqlSession = (DefaultSqlSession)sqlSessionFactory.openSession(ExecutorType.SIMPLE); long startTime = System.currentTimeMillis(); for (int i = 0; i < 10000; i++) { UserInfo userInfo = new UserInfo(); userInfo.setNickName("bbbc"); userInfo.setUserName("abcd"); userInfo.setBirthday(new Date()); userInfo.setRegisterTime(LocalDateTime.now()); userInfo.setEmail("1234"); defaultSqlSession.insert("com.example.mybatisanalyze.mapper.UserInfoMapper.insert", userInfo); } long endTime = System.currentTimeMillis(); System.out.println((endTime - startTime) + "ms");

    插入一万条数据
    simple:
    mybatis之执行器
    文章图片

    从图中可以看出花了11762ms
    reuse:
    代码中DefaultSqlSession defaultSqlSession = (DefaultSqlSession)sqlSessionFactory.openSession(ExecutorType.SIMPLE)这行改为DefaultSqlSession defaultSqlSession = (DefaultSqlSession)sqlSessionFactory.openSession(ExecutorType.REUSE);
    mybatis之执行器
    文章图片

    从图中可以看出花了10992ms
    batch:
    代码中DefaultSqlSession defaultSqlSession = (DefaultSqlSession)sqlSessionFactory.openSession(ExecutorType.SIMPLE)这行改为DefaultSqlSession defaultSqlSession = (DefaultSqlSession)sqlSessionFactory.openSession(ExecutorType.BATCH); 并且在for循环之后加个 commit 操作defaultSqlSession.commit();
    mybatis之执行器
    文章图片

    从图中可以看出花了6173ms
    结论:明显应该选择 batch
    各个执行器的实现原理
    mybatis 和执行器相关的逻辑都在org.apache.ibatis.executor包下
    mybatis之执行器
    文章图片

    先来看一下执行器接口org.apache.ibatis.executor.Executor
    mybatis之执行器
    文章图片

    可以看到基本就是数据库相关操作,再来看下继承体系
    mybatis之执行器
    文章图片

    可以看到我们所用的三种执行器都是子类,这里用到两种设计模式,分别是BaseExecutor中实现的模板模式和CachingExecutor中实现的装饰器模式。我们从入口开始看,入口在org.apache.ibatis.session.defaults.DefaultSqlSession,这里提供了基本的操作,我们基于selectOne方法进行分析,一直跟到org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds)会发现他本质调的就是执行器的查询操作
    mybatis之执行器
    文章图片

    而这个执行器哪里生成的呢?在org.apache.ibatis.session.defaults.DefaultSqlSessionFactory中我们调用openSession方法时,我们传入指定的执行器类型,也可以使用默认的,最终会在org.apache.ibatis.session.Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType)中创建执行器
    mybatis之执行器
    文章图片

    这里可以看到会根据传入的执行器类型来创建对应执行器,默认使用 simple,会使用CachingExecutor来进行一层包装。接着上面的 query 函数继续分析,我们知道它默认会走到org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)
    mybatis之执行器
    文章图片

    这里使用了装饰器模式,在原来的执行器功能基础上添加了二级缓存的功能。里面最终是调用org.apache.ibatis.executor.BaseExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)进行执行,这里用到了一级缓存,没有缓存的情况下会去数据库查,也就是调用方法queryFromDatabase
    mybatis之执行器
    文章图片

    这里就使用了模板模式,doQuery()由子类自己来实现,各个子类执行器的逻辑相对比较简单,这里就介绍一下BatchExecutor,其他可以自己去看
    mybatis之执行器
    文章图片

    这里维护了statementListbatchResultList来记录执行的指令以及存放对应结果的对象,在doUpdate的时候进行记录,最终会在handler.batch()中去调用PreparedStatementaddBatch方法,相当于把指令暂存到预编译器中。后续在我们做commit()的时候会去调用doFlushStatements方法去做批执行。
    总结
    【mybatis之执行器】本篇文章的干货还是比较多的,基本上将mybatis的执行器进行了比较详细的介绍以及如何选型,当然底层还涉及到连接池和 sql 驱动包的一些知识点,连接池后面讲,sql 预编译器批量执行的介绍,可以参考https://blog.csdn.net/bluelin...

    推荐阅读