转载

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
原文  http://dengchengchao.com/?p=1190
正文到此结束
Loading...