手写MyBatis第39弹:深入MyBatis BatchExecutor实现原理与最佳实践

🥂(❁´◡`❁)您的点赞👍➕评论📝➕收藏⭐是作者创作的最大动力🤞

💖📕🎉🔥 支持我:点赞👍+收藏⭐️+留言📝欢迎留言讨论

🔥🔥🔥(源码 + 调试运行 + 问题答疑)

🔥🔥🔥 有兴趣可以联系我。

我们常常在当下感到时间慢,觉得未来遥远,但一旦回头看,时间已经悄然流逝。对于未来,尽管如此,也应该保持一种从容的态度,相信未来仍有许多可能性等待着我们。

目录

一、批处理:为何需要特殊的Executor?

二、BatchExecutor的设计思路:维护批处理队列

[三、触发执行的机关:SqlSession的commit, flushStatements与Executor的联动](#三、触发执行的机关:SqlSession的commit, flushStatements与Executor的联动)

四、总结与最佳实践


  1. MyBatis执行器深度解析:Simple与Batch的update实现差异与批处理奥秘

  2. 手写MyBatis(四):Executor的批处理世界,从Simple到Batch的架构演进

  3. 高并发数据操作的利器:深入MyBatis BatchExecutor实现原理与最佳实践

  4. 超越Simple:手把手实现MyBatis批处理执行器,揭秘SqlSession提交与刷新的联动机制


在上一篇文章中,我们为SimpleExecutor成功实现了update方法,奠定了增删改操作的基础。然而,在现实的高并发、大数据量场景下,逐条执行SQL语句会带来巨大的网络I/O和数据库开销,成为性能的瓶颈。此时,JDBC提供的批处理(Batch)功能便闪亮登场。本文将深入MyBatis的批处理世界,探讨BatchExecutor的独特实现,并厘清SqlSession的提交、回滚与执行器之间的精妙协作关系。

一、批处理:为何需要特殊的Executor?

JDBC的批处理允许我们将多个SQL语句打包成一个"批次",一次性发送到数据库执行。这极大地减少了网络往返次数,数据库也可以对同一批操作进行优化,从而显著提升性能。

然而,批处理的使用模式与普通执行截然不同:

  • 普通执行(SimpleExecutor): update() -> executeUpdate() -> 立即执行 -> 返回结果。

  • 批处理执行(BatchExecutor): update() -> addBatch() -> 加入队列 -> commit() -> executeBatch() -> 批量执行 -> 返回结果。

这种"延迟执行"的特性意味着,我们不能在BatchExecutorupdate方法中立即执行SQL,而是需要将其缓存起来,等待一个统一的"触发信号"。这个根本性的差异,正是MyBatis需要设计不同Executor实现类的核心原因。

二、BatchExecutor的设计思路:维护批处理队列

BatchExecutor的核心在于维护一个批处理队列。这个队列需要存储什么?它需要存储每一次update调用所对应的PreparedStatement和参数信息,以便在最终执行时能够正确地重现这些操作。

让我们勾勒一下BatchExecutor的骨架:

java 复制代码
public class BatchExecutor implements Executor {
 ​
     // 存储批处理任务的队列
     private List<Statement> statementList = new ArrayList<>();
     private List<BatchResult> batchResultList = new ArrayList<>();
 ​
     // 当前事务对应的数据库连接
     private Connection connection;
     // 当前MappedStatement对应的PreparedStatement(简化模型,实际更复杂)
     private PreparedStatement currentPreparedStatement;
     private String currentSql;
     private MappedStatement currentMappedStatement;
 ​
     @Override
     public int update(MappedStatement ms, Object parameter) throws SQLException {
         // 1. 获取连接(通常从关联的Transaction中获取)
         if (connection == null) {
             connection = transaction.getConnection();
         }
 ​
         // 2. 检查当前SQL是否与上一次相同
         //    如果相同,可以复用PreparedStatement;如果不同,需要创建新的并处理上一个
         if (currentSql != null && currentSql.equals(ms.getSql())) {
             // SQL相同,复用currentPreparedStatement
         } else {
             // SQL不同,需要"刷新"上一个Statement(执行或加入队列),并创建新的
             if (currentPreparedStatement != null) {
                 // 将当前的PreparedStatement加入到批处理队列,并记录其影响
                 currentPreparedStatement.addBatch();
                 batchResultList.add(new BatchResult(currentMappedStatement, currentSql, parameter));
                 // 注意:这里并不执行executeBatch,只是加入队列
             }
             // 创建新的PreparedStatement
             currentPreparedStatement = connection.prepareStatement(ms.getSql());
             currentSql = ms.getSql();
             currentMappedStatement = ms;
         }
 ​
         // 3. 使用ParameterHandler处理参数,设置到当前的PreparedStatement中
         ParameterHandler parameterHandler = new DefaultParameterHandler(parameter);
         parameterHandler.setParameters(currentPreparedStatement);
 ​
         // 4. 将当前操作添加到批处理中(注意:不是执行)
         currentPreparedStatement.addBatch();
 ​
         // 5. 记录BatchResult
         batchResultList.add(new BatchResult(ms, ms.getSql(), parameter));
 ​
         // 6. 批处理操作不会立即返回影响行数,所以返回一个占位值(如BatchExecutor.BATCH_UPDATE_RETURN_VALUE)
         return BATCH_UPDATE_RETURN_VALUE;
     }
 }

(注:以上为简化模型,真实MyBatis的BatchExecutor实现更为复杂,会用一个statementMap来按SQL分类管理PreparedStatement)

从代码可以看出,BatchExecutorupdate方法更像是一个"筹备"阶段,它负责积累操作,而真正的执行则被推迟了。

三、触发执行的机关:SqlSession的commit, flushStatements与Executor的联动

那么,谁负责发出这个"触发信号"呢?答案就是SqlSessionSqlSession作为面向用户的API,其commit(), rollback(), flushStatements()等方法与Executor的生命周期紧密相关。

1. flushStatements():执行批处理的核心方法

这是批处理执行的核心。SqlSessionflushStatements()方法最终会调用ExecutorflushStatements()方法。

java 复制代码
 public class BatchExecutor extends BaseExecutor {
     @Override
     public List<BatchResult> flushStatements() throws SQLException {
         try {
             // 1. 执行所有批处理操作
             for (Statement stmt : statementList) {
                 // 执行executeBatch,并获取每个Statement的影响行数数组
                 int[] updateCounts = stmt.executeBatch();
                 // 将updateCounts与之前记录的BatchResult关联起来
                 // ...
             }
             // 2. 清空批处理队列
             statementList.clear();
             batchResultList.clear();
             // 3. 返回批处理结果
             return batchResults;
         } finally {
             // 关闭Statement等资源
         }
     }
 }

SqlSession的用户可以显式调用flushStatements()来强制执行当前积累的所有批处理操作,而不必提交事务。这在需要分批次提交大量数据时非常有用。

2. commit()rollback():事务的终结者

对于BatchExecutor来说,提交事务意味着必须首先执行所有批处理操作。

java 复制代码
public class DefaultSqlSession implements SqlSession {
     private Executor executor;
 ​
     @Override
     public void commit() {
         try {
             // 在提交事务之前,必须先执行(flush)所有批处理语句
             executor.flushStatements();
             // 然后才提交事务
             executor.commit(true);
         } catch (Exception e) {
             throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
         } finally {
             // ... 错误处理
         }
     }
 ​
     @Override
     public void rollback() {
         try {
             // 回滚事务,批处理队列中的操作会被丢弃,无需执行
             executor.rollback(true);
         } catch (Exception e) {
             throw ExceptionFactory.wrapException("Error rolling back transaction.  Cause: " + e, e);
         } finally {
             // ... 错误处理
         }
     }
 }

可以看到:

  • commit() :必须先行调用flushStatements(),确保所有积压的SQL都被执行,然后再提交数据库事务。

  • rollback():直接回滚事务即可,那些积压在内存中尚未执行的批处理操作直接丢弃,因为没有真正发送到数据库。

四、总结与最佳实践

通过对比SimpleExecutorBatchExecutorupdate实现,我们深刻理解了MyBatis执行器模式的优势:通过抽象接口,屏蔽了不同执行策略的复杂性,并通过与SqlSession的联动,提供了统一而灵活的用户体验。

给开发者的启示:

  1. 正确选择执行器: 在需要大量写操作的场景(如数据导入、批量更新),务必使用BatchExecutor(通过SqlSessionFactory.openSession(ExecutorType.BATCH)),性能提升可达数十倍甚至更高。

  2. 注意批处理的使用方式: 批处理操作需要显式调用sqlSession.commit()sqlSession.flushStatements()才会真正执行。忘记提交是初学者常犯的错误。

  3. 理解事务边界: 批处理操作同样受事务控制。在执行commit之前,所有操作都可以回滚。合理设置批处理大小(定期调用flushStatements),可以避免超大事务导致数据库锁等问题。

SimpleExecutorBatchExecutor,我们看到的不仅仅是一个功能的添加,更是一个框架在性能、资源与易用性 之间做出的精妙权衡与架构设计。在下一篇文章中,我们将探讨执行器的另一重要功能:缓存,看MyBatis如何通过CachingExecutor为性能再次加码。


💖学习知识需费心,
📕整理归纳更费神。
🎉源码免费人人喜,
🔥码农福利等你领!

💖常来我家多看看,
📕我是程序员扣棣,
🎉感谢支持常陪伴,
🔥点赞关注别忘记!

💖山高路远坑又深,
📕大军纵横任驰奔,
🎉谁敢横刀立马行?
🔥唯有点赞+关注成!

相关推荐
小码哥_常2 小时前
Spring Boot配置diff:解锁配置管理新姿势
前端
小码哥_常2 小时前
告别onActivityResult!Android数据回传的3大痛点与终极解决方案
前端
hhcccchh2 小时前
1.2 CSS 基础选择器、盒模型、flex 布局、grid 布局
前端·css·css3
修己xj2 小时前
Markdown 里写公式,别只知道 LaTeX!试试 HTML 标签,简单到飞起
html
专吃海绵宝宝菠萝屋的派大星3 小时前
使用Dify对接自己开发的mcp
java·服务器·前端
爱分享的阿Q3 小时前
Rust加WebAssembly前端性能革命实践指南
前端·rust·wasm
蓝黑20203 小时前
Vue的 value=“1“ 和 :value=“1“ 有什么区别
前端·javascript·vue
小李子呢02113 小时前
前端八股6---v-model双向绑定
前端·javascript·算法
He少年3 小时前
【基础知识、Skill、Rules和MCP案例介绍】
java·前端·python
史迪仔01123 小时前
[QML] QML IMage图像处理
开发语言·前端·javascript·c++·qt