MyBatis 的秘密(二)Executor

Executor

MyBatis中有关Executor的配置如下:

设置名 描述 有效值 默认值
defaultExecutorType 配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(prepared statements); BATCH 执行器将重用语句并执行批量更新。 SIMPLE REUSE BATCH SIMPLE

也就是说,在MyBatis中有三种Executor:

  • SimpleExecutor: 就是普通的执行器。

  • ReuseExecutor : 执行器会重用预处理语句(PreparedStatement

  • BatchExecutor : 批量执行器,底层调用JDBCStatement#batch()


Executor的作用?

首先看SqlSession的一个查询方法的源代码:

DefaultSqlSession#selectList()

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds){
    //获取对应的配置
    MappedStatement ms = configuration.getMappedStatement(statement);
    //调用`Executor`的查询
    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
}

从上面的源码我们可以看出来,Executor实际上就是一个打工仔。所有SqlSession执行的方法,基本上都是委托给Executor执行的。

那以上3中执行器代码中有什么不同呢?


MyBatis中,如果开启了二级缓存,那么会使用MyBatis的二级缓存,而这个二级缓存的作用点,便在于Executor的其中一个。

换一种意思便是,在MyBatis中,Executor的实现一共有4个,另外一个为CachingExecutor

CacheingExecutor作为其他3个Executor的装饰者,本身维护了一套缓存机制,底层的调用依然是使用的其他3个Executor,因此这里暂时先跳过;

其他3个Executor都继承自BaseExecutorBaseExecutor中实现了一些通用的方法,

Executor中,所有的SQL都被归为两类 : 修改(增,删,改) 和 查询 (查),因此Executor中将所有的SQL操作都归为了两个方法

BaseExecutor

BaseExecutor 很像是设计模式中的模板方法模式,它实现了Executor的一些通用方法,比如正常的逻辑判断,一级缓存,日志等,然后在真正需要调用逻辑的时候再调用abstract方法,比如:

@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
    //添加日志信息
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    //逻辑判断
    if (closed) {
        throw new ExecutorException("Executor was closed.");
    }
    //清空一级缓存
    clearLocalCache();
    //获取结果
    return doUpdate(ms, parameter);
}

其中doUpdate()便是需要子类自己实现

SimpleExecutor

SimpleExecutor是一个最简单的执行器,没有做额外的操作。

首先看Executorupdate()方法

SimpleExecutor#doUpdate()

@Override
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    Statement stmt = null;
    try {
        //获取配置
        Configuration configuration = ms.getConfiguration();
        //创建`StatementHandler`
        StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
        //初始化statement
        stmt = prepareStatement(handler, ms.getStatementLog());
        //执行具体的方法
        return handler.update(stmt);
    } finally {
        closeStatement(stmt);
    }
}

可以看到这里大概就是获取配置,创建StatementHandler,初始化Statement,然后获取结果。

几乎所有的操作都交给了StatementHandler操作了,为什么还有分一层Executor呢?

答案在于prepareStatement和调用handler的方法。

前面说过3种Executor的不同功能,对于ReuseExecutor 会重用Statement,对于BatchExecutor会执行批处理。

下面看看体现这些功能的代码:

ReuseExecutor#prepareStatement()

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    //获取处理SQL
    BoundSql boundSql = handler.getBoundSql();
    String sql = boundSql.getSql();
    //查看缓存中时候已经有statement
    //如果存在,则直接使用
    if (hasStatementFor(sql)) {
        stmt = getStatement(sql);
        applyTransactionTimeout(stmt);
    } else {
        //如果不存在,则新建statement,并缓存起来
        Connection connection = getConnection(statementLog);
        stmt = handler.prepare(connection, transaction.getTimeout());
        putStatement(sql, stmt);
    }
    handler.parameterize(stmt);
    return stmt;
}

BatchExecutor

BatchExecutor是在我们需要循环/批量执行某些操作的时候,可以进行试用,其底层使用的statment#addBatch()方法,一般对于mysql,要想此方法发挥其性能的优点,需要在连接的时候添加以下两个命令:

useServerPrepStmts=false
rewriteBatchedStatements=true

BatchExecutor#doUpdate()

@Override
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
    final Configuration configuration = ms.getConfiguration();
    final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null);
    final BoundSql boundSql = handler.getBoundSql();
    final String sql = boundSql.getSql();
    final Statement stmt;
    //判断是和上一次执行SQL是同一条SQL
    //并且statment是否也相同
    //如果相同,则继续执行addBatch()
    //否则进行新建
    if (sql.equals(currentSql) && ms.equals(currentStatement)) {
        //获取最后一个statement的坐标
        //为什么不用栈???
        int last = statementList.size() - 1;
        //获取最后一次添加的statement
        stmt = statementList.get(last);
        //设置statement超时时间
        //取事务和statement设置的较小值
        applyTransactionTimeout(stmt);
        //设置参数
        handler.parameterize(stmt);
        //保存参数,后续其他业务逻辑需要,比如KeyGenerator
        BatchResult batchResult = batchResultList.get(last);
        batchResult.addParameterObject(parameterObject);
    } else {
        //否则,新建一个statement
        Connection connection = getConnection(ms.getStatementLog());
        stmt = handler.prepare(connection, transaction.getTimeout());
        handler.parameterize(stmt);    //fix Issues 322
        currentSql = sql;
        currentStatement = ms;
        //每新建一个statement,都将其放在List中
        //方便后续处理
        statementList.add(stmt);
        batchResultList.add(new BatchResult(ms, sql, parameterObject));
    }
    //调用statement 的batch方法
    handler.batch(stmt);
    return BATCH_UPDATE_RETURN_VALUE;
}

可能有人不大理解这段代码。先贴出 JDBCbatch使用方式:

//插入1000条测试代码
 PreparedStatement psts = conn.prepareStatement(sql);  
for(int i=0;i<1000;i++){
    psts.setString(1,"123");  
    psts.setString(2,"1234");
    psts.addBatch(); 
}
psts.executeBatch();
psts.commit();

可以看到,JDBCaddBatch需要不断的向PreparedStatement添加参数,然后调用addBatch()方法。

上面的逻辑便是,首先判断添加的参数是不是需要循环添加的,如果是,则继续添加,如果SQL不一样了,那么需要新建一个Statement,然后再继续添加参数。


继续看我们可以看到,JDBC在添加完参数以后,还需要执行依据executeBatch()去真正的批量执行SQL,那么MyBatis将这一步放在哪里了呢?


BatchExecutor#doFlushStatements()

@Override
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
    try {
        List<BatchResult> results = new ArrayList<>();
        //判断是否已经回滚
        if (isRollback) {
            return Collections.emptyList();
        }
        //遍历处理所有的statement
        for (int i = 0, n = statementList.size(); i < n; i++) {
            Statement stmt = statementList.get(i);
            //设置超时时间
            applyTransactionTimeout(stmt);
            BatchResult batchResult = batchResultList.get(i);
            try {
                //执行executeBatch
                batchResult.setUpdateCounts(stmt.executeBatch());
                //获取对应的statmenment
                MappedStatement ms = batchResult.getMappedStatement();
                //获取参数
                List<Object> parameterObjects = batchResult.getParameterObjects();
                //获取对应的主键处理器
                KeyGenerator keyGenerator = ms.getKeyGenerator();
                //处理主键
                if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
                    Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
                    jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
                } else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { //issue #141
                    for (Object parameter : parameterObjects) {
                        keyGenerator.processAfter(this, ms, stmt, parameter);
                    }
                }
                // Close statement to close cursor #1109
                closeStatement(stmt);
            } catch (BatchUpdateException e) {
                StringBuilder message = new StringBuilder();
                message.append(batchResult.getMappedStatement().getId())
                    .append(" (batch index #")
                    .append(i + 1)
                    .append(")")
                    .append(" failed.");
                if (i > 0) {
                    message.append(" ")
                        .append(i)
                        .append(" prior sub executor(s) completed successfully, but will be rolled back.");
                }
                throw new BatchExecutorException(message.toString(), e, results, batchResult);
            }
            results.add(batchResult);
        }
        return results;
    } finally {
        for (Statement stmt : statementList) {
            closeStatement(stmt);
        }
        currentSql = null;
        statementList.clear();
        batchResultList.clear();
    }
}

可以看到,这里代码虽然多,但是其实就是做了两个动作:

  • 执行executeBatch()
  • 处理执行完后的主键

而这个flushStatement()方法同时会在commit()方法中被调用,因此一般也不需要我们手动调用。


ReuseExecutor

ReuseExecutor 是作为一个能将Statement缓存起来进行复用的执行器。和其他执行主要不同的地方在于prepareStatement:

ReuseExecutor#prepareStatement()

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    BoundSql boundSql = handler.getBoundSql();
    String sql = boundSql.getSql();
    //通过sql 查询是否有缓存的`statement`
    if (hasStatementFor(sql)) {
        //如果有,直接进行复用
        stmt = getStatement(sql);
        //设置超时的值
        applyTransactionTimeout(stmt);
    } else {
        //没有则新建,并进行缓存
        Connection connection = getConnection(statementLog);
        stmt = handler.prepare(connection, transaction.getTimeout());
        putStatement(sql, stmt);
    }
    handler.parameterize(stmt);
    return stmt;
}

可以看到,这样是节约了新建statement的时间,对于PreperStatement来说,同时也节约了预编译SQL的时间。

但是我们可以发现一般默认的Executor并不是REUSE,而是SIMPLE,为什么呢?因为从MyBatis的执行流程来看,一个Executor属于一个SqlSession,而在MyBatis中,并不推荐SqlSession复用,一般一个方法对应于有一个SqlSession,使用完以后需要关闭,因此很少存在能命中的情况。

当然如果是for循环执行的话,那么应该建议改成ReuseExecutor或者<for>改写SQL,因此一般默认SIMPLE级别就足够使用。


以上便是MyBatisExecutor的秘密。这里我们再总结下已经查看的知识:

  • MyBatis为了方便控制数据库的事务,通过在中间添加了一层Transaction,使得MyBatis可以接受其他的方式控制事务
  • 对于MyBatis的默认事务配置,直接使用JDBC即可,也就是最简单的事务控制。
  • MyBatis中设有3种不同的Executor,分别是SIMPLE,REUSE,BATCH ,默认为SIMPLE,REUSE缓存了statmentBATCH底层使用了JDBCaddBatch()对应的方法
  • 如果要使得BATCH发挥效率,需要在MySql的链接命令中添加useServerPrepStmts=false&rewriteBatchedStatements=true