1. 问题现象与背景分析
上周在开发一个批量导入功能时,遇到了一个典型的MyBatis-Plus事务问题:在异步线程中使用saveBatch方法批量插入数据时,发现数据没有成功持久化到数据库。主线程的事务已经提交,但异步任务中的批量操作却"神秘消失"了。这个问题在Spring + MyBatis-Plus的技术栈中其实相当典型,值得深入剖析。
先还原下问题场景:我们有个商品批量导入的需求,主线程处理完Excel解析后,将数据分发给多个异步线程进行并发处理。每个线程内部使用MyBatis-Plus的saveBatch方法批量保存数据到MySQL。开发时本地测试一切正常,但上线后发现部分批次的数据丢失,日志显示执行了insert操作但数据库查不到记录。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 事务传播机制的核心原理
2.1 Spring事务的线程隔离特性
Spring的事务管理本质上是基于ThreadLocal实现的,这意味着事务上下文是与线程绑定的。当我们使用@Transactional注解时,Spring会在当前线程创建事务上下文,而新建的异步线程无法继承这个上下文。这就是为什么在异步线程中执行数据库操作时,看似在同一个"事务"里,实际上却处于不同的事务上下文中。
关键点在于:默认情况下,@Async注解标记的方法会使用单独的线程池执行,这些线程与主线程的事务上下文完全隔离。即使主方法有@Transactional注解,异步方法内部的操作也不会自动参与这个事务。
2.2 MyBatis-Plus的saveBatch实现机制
MyBatis-Plus的saveBatch方法表面上看是个简单的批量插入,但其内部实现有几个关键细节:
- 默认情况下,它并不是真正的批量SQL(虽然方法名容易让人误解),而是通过for循环+单条insert的方式实现
- 只有在开启事务的情况下,这些单条insert才会被当作一个原子操作
- 如果没有显式事务,每条insert都会自动提交
查看源码可以发现,SqlHelper类中的executeBatch方法会判断当前是否存在事务:如果有事务,就批量执行但不提交;没有事务则立即提交每条语句。
3. 问题复现与根因定位
3.1 最小化复现代码
java复制@Service
public class ProductImportService {
@Autowired
private ProductMapper productMapper;
@Autowired
private AsyncService asyncService;
@Transactional
public void importProducts(List<Product> products) {
// 主线程操作
productMapper.insert(products.get(0));
// 异步处理剩余数据
asyncService.asyncProcess(products.subList(1, products.size()));
}
}
@Service
public class AsyncService {
@Async
public void asyncProcess(List<Product> products) {
// 这里使用的saveBatch实际上不会加入主事务
productService.saveBatch(products);
}
}
3.2 问题根因分析
通过调试和日志分析,可以确认问题发生的完整链条:
- 主线程开启事务,插入第一条记录(未提交)
- 异步线程执行
saveBatch,由于没有事务上下文:- 每条insert都自动提交
- 但此时主事务还未提交,可能持有表锁
- 根据数据库隔离级别不同,可能出现:
- 在READ_COMM
