MyBatis-Plus saveBatch异步事务问题分析与解决方案
1. 问题背景与现象描述最近在开发一个电商订单处理系统时遇到了一个棘手的问题使用MyBatis-Plus的saveBatch方法在异步线程中批量插入数据时发现事务没有正常提交。具体表现为系统使用Spring Boot MyBatis-Plus架构订单创建后需要异步处理库存扣减和日志记录使用Async注解标记的异步方法中调用了saveBatch批量插入操作日志日志表中有部分记录插入成功部分记录丢失没有抛出任何异常但数据不完整这个问题在测试环境偶发出现但在高并发压测时几乎必现。经过排查发现这与MyBatis-Plus的批量操作机制、Spring事务管理以及异步线程处理有密切关系。2. MyBatis-Plus saveBatch原理剖析2.1 saveBatch的默认实现MyBatis-Plus的saveBatch方法默认实现是这样的// MyBatis-Plus 3.x版本的默认实现 Transactional(rollbackFor Exception.class) Override public boolean saveBatch(CollectionT entityList, int batchSize) { String sqlStatement sqlStatement(SqlMethod.INSERT_ONE); return executeBatch(entityList, batchSize, (sqlSession, entity) - { sqlSession.insert(sqlStatement, entity); }); }关键点方法本身带有Transactional注解使用MyBatis的SqlSession执行批量插入默认batchSize为10002.2 批量操作的执行流程saveBatch的实际执行流程可以分为以下几个步骤开启事务由Spring管理对集合进行分片处理根据batchSize对每个分片执行批量插入提交事务如果成功或回滚如果失败问题在于当这个方法在异步线程中执行时第4步的事务提交可能不会按预期工作。3. 异步环境中的事务问题3.1 Spring事务管理机制Spring的事务管理是基于ThreadLocal实现的关键点包括事务上下文存储在ThreadLocal中Transactional注解的事务传播行为默认是REQUIRED异步方法会使用新的线程执行无法继承原有的事务上下文3.2 Async与事务的交互当我们在异步方法中使用Transactional时会遇到以下问题异步方法本身需要Async注解如果异步方法内部有Transactional会创建新的事务这两个注解的执行顺序和交互需要特别注意3.3 典型的问题场景在我们的案例中代码结构大致如下Service public class OrderService { Autowired private AsyncLogService asyncLogService; Transactional public void createOrder(OrderDTO dto) { // 订单创建逻辑... asyncLogService.saveOperationLog(logs); } } Service public class AsyncLogService { Async Transactional(propagation Propagation.REQUIRES_NEW) public void saveOperationLog(ListOperationLog logs) { logMapper.saveBatch(logs); // 使用MyBatis-Plus的saveBatch } }这种情况下虽然两个方法都有Transactional注解但由于异步执行事务可能无法正常提交。4. 问题排查过程4.1 复现问题为了准确复现问题我们设计了以下测试方案准备1000条测试数据在异步方法中调用saveBatch观察数据库中的记录数量检查日志是否有异常测试结果有时插入全部成功有时部分成功如插入300条没有异常日志4.2 日志分析通过增加事务相关的日志配置logging.level.org.springframework.transactionDEBUG logging.level.org.mybatisTRACE从日志中可以观察到主线程的事务正常开启和提交异步线程中的事务有时没有提交日志没有回滚日志4.3 线程池配置检查发现项目中配置了自定义的线程池Configuration EnableAsync public class AsyncConfig implements AsyncConfigurer { Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(100); executor.setThreadNamePrefix(async-); executor.initialize(); return executor; } }线程池配置可能导致任务被拒绝或线程被回收影响事务提交。5. 解决方案5.1 方案一使用编程式事务管理改造异步方法使用TransactionTemplateService public class AsyncLogService { Autowired private TransactionTemplate transactionTemplate; Async public void saveOperationLog(ListOperationLog logs) { transactionTemplate.execute(status - { try { logMapper.saveBatch(logs); return Boolean.TRUE; } catch (Exception e) { status.setRollbackOnly(); throw e; } }); } }优点明确控制事务边界避免注解方式的问题缺点代码稍显冗长5.2 方案二调整事务传播行为修改Transactional的传播行为Async Transactional(propagation Propagation.NESTED) public void saveOperationLog(ListOperationLog logs) { logMapper.saveBatch(logs); }注意NESTED需要数据库支持保存点不是所有场景都适用5.3 方案三使用同步批量插入如果不必须异步可以改为同步执行Service public class OrderService { Transactional public void createOrder(OrderDTO dto) { // 订单创建逻辑... logMapper.saveBatch(logs); // 同步执行 } }最简单可靠但可能影响性能。5.4 最终采用的方案我们最终选择了方案一结合以下优化增加事务超时设置添加重试机制完善日志记录完整实现Async public void saveOperationLog(ListOperationLog logs) { transactionTemplate.setTimeout(30); // 30秒超时 transactionTemplate.execute(status - { try { int retryCount 0; while (retryCount 3) { try { logMapper.saveBatch(logs); return Boolean.TRUE; } catch (Exception e) { retryCount; if (retryCount 3) { throw e; } Thread.sleep(1000 * retryCount); } } return Boolean.FALSE; } catch (Exception e) { log.error(保存操作日志失败, e); status.setRollbackOnly(); throw new RuntimeException(保存操作日志失败, e); } }); }6. 深入原理为什么会出现这个问题6.1 MyBatis-Plus批量操作的本质虽然叫批量插入但默认实现其实是循环单条插入不是真正的JDBC批量addBatch/executeBatch每条insert都是独立的SQL语句依赖事务保证原子性6.2 Spring异步执行的原理Async的工作机制通过AOP代理拦截方法调用提交到线程池执行原始线程继续执行新线程中方法执行6.3 事务失效的根本原因综合来看问题根源在于异步线程可能被突然终止如线程池回收事务提交发生在异步线程中Spring无法保证异步线程的事务一定会提交MyBatis-Plus的批量不是原子操作7. 性能优化建议7.1 使用真正的批量插入可以重写saveBatch方法使用JDBC的批量操作public class CustomServiceImplM extends BaseMapperT, T extends ServiceImplM, T { Override Transactional public boolean saveBatch(CollectionT entityList, int batchSize) { try (SqlSession batchSqlSession sqlSessionBatch()) { int i 0; for (T entity : entityList) { batchSqlSession.insert(sqlStatement(SqlMethod.INSERT_ONE), entity); if (i 1 i % batchSize 0) { batchSqlSession.flushStatements(); } i; } batchSqlSession.flushStatements(); return true; } } }7.2 调整批量大小根据数据库性能调整batchSizeMySQL建议500-1000Oracle建议100-200SQL Server建议1000-20007.3 使用多线程批量插入对于大数据量可以结合多线程public void batchInsertConcurrent(ListData dataList) { int threadCount 4; int batchSize dataList.size() / threadCount; ExecutorService executor Executors.newFixedThreadPool(threadCount); ListFuture? futures new ArrayList(); for (int i 0; i threadCount; i) { int from i * batchSize; int to (i threadCount - 1) ? dataList.size() : (i 1) * batchSize; ListData subList dataList.subList(from, to); futures.add(executor.submit(() - { transactionTemplate.execute(status - { customService.saveBatch(subList); return null; }); })); } for (Future? future : futures) { try { future.get(); } catch (Exception e) { // 处理异常 } } executor.shutdown(); }8. 其他注意事项8.1 事务隔离级别的影响在高并发下还需要考虑隔离级别READ_COMMITTED可能导致幻读SERIALIZABLE性能影响大建议根据业务需求选择合适的隔离级别8.2 连接池配置确保连接池配置合理足够大的最大连接数合理的超时设置适当的验证查询例如HikariCP配置spring.datasource.hikari.maximum-pool-size20 spring.datasource.hikari.connection-timeout30000 spring.datasource.hikari.validation-timeout5000 spring.datasource.hikari.leak-detection-threshold600008.3 监控与告警建议添加以下监控事务执行时间监控批量操作成功率监控线程池使用情况监控可以使用Micrometer Prometheus Grafana实现。9. 常见问题解答9.1 为什么部分数据插入成功了这是因为MyBatis-Plus的saveBatch默认不是原子操作。在事务提交前部分插入已经执行但如果事务最终没有提交这些已执行的插入可能会被保留取决于数据库的具体实现。9.2 如何确定是事务问题可以通过以下方法验证在方法结束后手动抛出异常看是否回滚检查数据库事务日志使用Spring的TransactionSynchronizationManager.isActualTransactionActive()9.3 除了saveBatch还有其他方法吗可以考虑使用MyBatis的 标签实现批量插入使用JDBC的addBatch/executeBatch使用存储过程处理批量数据9.4 异步事务的最佳实践是什么建议避免在异步方法中进行复杂的多步骤事务如果必须使用事务确保有完善的错误处理和重试机制考虑使用消息队列实现最终一致性监控异步任务的执行情况10. 总结与个人建议经过这次问题排查我总结了以下几点经验不要想当然地认为批量操作就是原子的要了解框架的具体实现异步和事务结合使用时需要格外小心生产环境中的事务问题往往在高压下才会暴露完善的日志和监控是快速定位问题的关键在实际项目中我建议对于关键业务操作优先考虑同步执行如果必须异步考虑使用消息队列等更可靠的机制对批量操作进行充分的压力测试编写详细的文档记录这些坑避免团队成员重复踩坑最后记住一个原则分布式系统没有完美的事务解决方案我们需要根据业务特点在一致性和性能之间找到平衡点。

相关新闻

Pinpoint全链路监控:从分布式追踪原理到生产环境部署实战

Pinpoint全链路监控:从分布式追踪原理到生产环境部署实战

1. 从单体到微服务:为什么我们需要全链路监控?如果你是从单体应用时代一路走过来的开发者,大概还记得那种“岁月静好”的日子。一个应用,一个数据库,出了问题,登录服务器,看日志,查数…

2026/10/13 15:29:24 阅读更多 →
MPC Video Renderer终极指南:RTX HDR加持的专业级视频渲染器

MPC Video Renderer终极指南:RTX HDR加持的专业级视频渲染器

MPC Video Renderer终极指南:RTX HDR加持的专业级视频渲染器 【免费下载链接】VideoRenderer RTX HDR modded into MPC-VideoRenderer. 项目地址: https://gitcode.com/gh_mirrors/vid/VideoRenderer 想要在Windows系统上获得影院级的视频播放体验吗&#xf…

2026/9/30 10:12:09 阅读更多 →
Android UID详解:应用身份标识、沙盒隔离与共享机制

Android UID详解:应用身份标识、沙盒隔离与共享机制

1. 项目概述:理解Android世界的“身份证”系统在Android开发或者系统维护的日常里,你是否遇到过这样的场景:调试一个应用时,发现它无法访问另一个应用创建的某个文件;或者,在分析系统日志时,看到…

2026/10/12 13:11:23 阅读更多 →

最新新闻

Java基础探秘:JSON解析与反射机制如何协同工作

Java基础探秘:JSON解析与反射机制如何协同工作

最近在写一个接口的时候,我盯着IDE里那几十行setter调用,脑子里突然冒出一个问题:前端传过来一段JSON字符串,我一行代码就把它变成了业务对象,中间到底是谁在帮我完成键名和字段的匹配?顺着这个疑问查下去&…

2026/10/14 6:41:29 阅读更多 →
QGIS 加载天地图出现灰色影像:层级参数 l 与 z 的坑,附四种返回码对照

QGIS 加载天地图出现灰色影像:层级参数 l 与 z 的坑,附四种返回码对照

场景QGIS 里挂天地图底图,图层加成功了,瓦片也在刷,画面却是一片灰,每个格子上写着"此级别下,该区域无影像"。打开浏览器 F12 一看,状态码 200,图片也确实回来了。这时候大多数人会往…

2026/10/14 6:41:29 阅读更多 →
Agent通信协议选型指南:JSON-RPC、MCP、A2A与ACP的分层实践

Agent通信协议选型指南:JSON-RPC、MCP、A2A与ACP的分层实践

1. 为什么 Agent 通信协议突然“百花齐放”过去一年里,只要你在做 AI Agent 相关的开发,几乎绕不开一个困惑:同样是让两个智能体互相说话,怎么一会儿是 JSON-RPC,一会儿是 MCP,转头又冒出 A2A、ACP&#xf…

2026/10/14 6:41:29 阅读更多 →
Chat2DB 实战:AI 生成 SQL 的数据库管理工具与避坑指南

Chat2DB 实战:AI 生成 SQL 的数据库管理工具与避坑指南

简介:Chat2DB项目代码包是一份集成人工智能能力的开源数据库管理工具示例,面向需要降低数据库操作门槛的开发者、数据分析师以及对自然语言生成SQL感兴趣的技术人员。整个资源共包含3个文件,涵盖inscode入口文件、html介绍文档以及gitignore配…

2026/10/14 6:41:29 阅读更多 →
PS5串流实战:AnyPS5配置与网络调优全攻略

PS5串流实战:AnyPS5配置与网络调优全攻略

把一台PS5玩出三台机器的效果,是不少玩家都在琢磨的事。AnyPS5这个项目,说白了就是干这件事:让PS5不再被客厅电视绑住,而是把画面和操作实时串流到你手边的任何一块屏幕上——笔记本、平板、手机、甚至办公电脑。我在这个方向上前…

2026/10/14 6:41:29 阅读更多 →
C#调用WebService获取PDF转图片:选型与踩坑实战

C#调用WebService获取PDF转图片:选型与踩坑实战

做C#开发这几年,遇到“调用WebService获取PDF文件再转换为图片”这种需求不算少。可能是一个审批系统需要预览合同,也可能是一个报表平台需要把服务端生成的文件转成缩略图展示。总之核心工作就两件事:把远程文件拿下来,再在本地转…

2026/10/14 6:40:29 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →