MyBatis-Plus saveBatch 在异步线程中事务未提交问题排查与修复

问题背景

在批量导入功能中,发现 saveBatch 方法被调用并返回 true,但数据库中没有新记录。调用时传入 164 条实体,执行无报错,但数据未持久化。

问题现象

java 复制代码
// 异步线程中执行
executorService.execute(() -> {
    boolean saveResult = service.saveBatch(entityList);
    // saveResult = true,但数据库中没有记录
});

排查过程

1. 初步假设

  • 假设A:异步线程中事务未提交
  • 假设B:数据构建阶段字段为 null
  • 假设C:关联数据查询返回 null
  • 假设D:构建的对象缺少必填字段
  • 假设E:saveBatch 返回 true 但实际未插入
  • 假设F:插入后立即查询不到(事务隔离)
  • 假设G:异步线程异常被吞掉
  • 假设H:异步线程未执行

2. 添加调试日志

在关键位置添加日志,追踪执行流程:

java 复制代码
// 记录异步线程执行
log.info("异步线程开始执行,线程名: {}", Thread.currentThread().getName());

// 记录数据构建
log.info("构建的实体对象,id: {}, status: {}", 
    entity.getId(), entity.getStatus());

// 记录 saveBatch 调用
log.info("准备调用saveBatch,listSize: {}", entityList.size());

// 记录 saveBatch 结果
log.info("saveBatch执行结果,saveResult: {}, listSize: {}", 
    saveResult, entityList.size());

// 验证插入结果
long actualCount = service.count(...);
log.info("验证插入结果,expectedCount: {}, actualCount: {}", 
    entityList.size(), actualCount);

3. 日志分析结果

日志显示:

  • 异步线程已执行
  • 数据字段正常
  • 关联数据查询正常
  • 对象字段完整
  • saveBatch 已调用(listSize=164)
  • saveBatch 返回 true
  • 验证插入结果:expectedCount=164, actualCount=328

关键发现:实际记录数是期望的 2 倍,说明:

  1. saveBatch 确实执行了
  2. 记录确实插入了
  3. 但查询时包含了旧记录

进一步分析发现:虽然记录已插入,但可能因为事务未提交,导致查询时看不到新数据。

根本原因

问题1:异步线程中事务未提交

在异步线程中执行 saveBatch 时,如果没有显式事务管理,可能出现:

  • 连接池返回的连接的 autoCommit 状态不一致
  • 批量插入需要显式事务才能正确提交
  • 异步线程不在 Spring 事务管理范围内

问题2:事务范围过大

最初将整个方法放在一个事务中:

java 复制代码
@Transactional(rollbackFor = Exception.class)
public Result<?> processBatchData(...) {
    // 文件读取、验证、外部服务调用、数据保存都在一个事务中
    // 事务锁定时间过长,可能几十秒
}

这会导致:

  • 数据库锁持有时间过长
  • 影响并发性能
  • 可能超时

修复方案

方案演进

阶段1:添加事务管理(异步场景)
java 复制代码
// 使用 TransactionTemplate 确保事务提交
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
transactionTemplate.execute(status -> {
    service.saveBatch(entityList);
    return true;
});
阶段2:改为同步执行
java 复制代码
@Transactional(rollbackFor = Exception.class)
public Result<?> processBatchData(...) {
    // 同步执行,Spring 自动管理事务
    service.saveBatch(entityList);
}
阶段3:缩小事务范围
java 复制代码
// 主方法不使用事务
public Result<?> processBatchData(...) {
    // 文件读取、验证、外部服务调用(无事务)
    
    // 只对关键操作使用事务
    saveBatchData(entityList, companyId);
}

@Transactional(rollbackFor = Exception.class)
private void saveBatchData(...) {
    service.saveBatch(entityList);
}
阶段4:分批处理 + 事务合并(最终方案)
java 复制代码
// 分批处理,每批在同一事务中
List<List<Entity>> batchList = Lists.partition(entityList, 10);

for (List<Entity> batch : batchList) {
    // 每批的更新关联数据和批量插入放在同一个事务中
    service.updateAndSaveBatch(companyId, batch, relatedDataMap, userName);
}

@Transactional(rollbackFor = Exception.class)
public void updateAndSaveBatch(...) {
    // 更新关联数据
    updateRelatedData(req, userName);
    
    // 批量插入
    service.saveBatch(entityList);
}

最终方案详解

1. 事务范围优化
java 复制代码
// 主方法:无事务
public Result<?> processBatchData(...) {
    // 文件读取(无事务)
    // 数据验证(无事务)
    // 外部服务调用(无事务)
    
    // 分批处理,每批独立事务
    for (List<Entity> batch : batchList) {
        service.updateAndSaveBatch(...);
    }
}

// 批次方法:小事务
@Transactional(rollbackFor = Exception.class)
public void updateAndSaveBatch(...) {
    // 更新关联数据(10条记录)
    updateRelatedData(req, userName);
    
    // 批量插入(10条记录)
    service.saveBatch(entityList);
}
2. 性能优化

批量查询关联数据,避免 N+1 查询:

java 复制代码
// 优化前:循环中每次查询
entityList.stream().map(e -> {
    RelatedData data = relatedDataService.queryById(e.getId());
    // N次数据库查询
})

// 优化后:批量查询一次
Map<String, RelatedData> dataMap = relatedDataService.list(
    Wrappers.lambdaQuery(RelatedData.class)
        .in(RelatedData::getId, idList))
    .stream()
    .collect(Collectors.toMap(RelatedData::getId, Function.identity()));

entityList.stream().map(e -> {
    RelatedData data = dataMap.get(e.getId());
    // 从Map中获取,无数据库查询
})
3. 事务结构
复制代码
主方法(无事务)
├── 文件读取(无事务)
├── 数据验证(无事务)
├── 外部服务调用(无事务)
└── 分批处理
    ├── 第1批(事务1,10条记录)
    │   ├── 更新关联数据
    │   └── 批量插入
    ├── 第2批(事务2,10条记录)
    │   ├── 更新关联数据
    │   └── 批量插入
    └── ...

关键要点

1. 异步线程中的事务管理

  • 异步线程不在 Spring 事务管理范围内
  • 必须使用 TransactionTemplate@Transactional + 代理调用
  • 通过接口调用确保走 Spring 代理

2. 事务范围设计原则

  • 只读操作不使用事务
  • 外部服务调用不使用事务
  • 关键数据操作使用小事务
  • 相关操作放在同一事务中保证原子性

3. 性能优化技巧

  • 批量查询替代循环查询(避免 N+1)
  • 分批处理减少事务锁定时间
  • 合理设置批次大小(10-100 条)

4. 调试方法

  • 添加详细日志追踪执行流程
  • 验证数据状态(构建前后、保存前后)
  • 检查事务是否生效(TransactionSynchronizationManager.isActualTransactionActive()

总结

  1. 问题根源:异步线程中事务未正确提交
  2. 解决方案:使用 @Transactional + 接口代理调用,确保事务生效
  3. 性能优化:缩小事务范围,分批处理,批量查询
  4. 最佳实践:事务只用于关键数据操作,保持事务范围最小

通过以上优化,既解决了数据插入问题,又提升了系统性能和并发能力。

参考代码

java 复制代码
// 最终实现
public Result<?> processBatchData(File file, String userName) {
    // 1. 文件读取、验证、外部服务调用(无事务)
    List<Entity> entityList = parseFile(file);
    validateData(entityList);
    callExternalService(errorList);
    
    // 2. 批量查询关联数据(优化N+1问题)
    List<String> idList = entityList.stream()
        .map(Entity::getId)
        .collect(Collectors.toList());
    Map<String, RelatedData> dataMap = relatedDataService.list(
        Wrappers.lambdaQuery(RelatedData.class)
            .in(RelatedData::getId, idList))
        .stream()
        .collect(Collectors.toMap(RelatedData::getId, Function.identity()));
    
    // 3. 按业务分组处理
    Map<String, List<Entity>> groupMap = entityList.stream()
        .collect(Collectors.groupingBy(Entity::getGroupId));
    
    groupMap.forEach((groupId, groupList) -> {
        // 4. 分批处理,每批独立事务
        List<List<Entity>> batchList = Lists.partition(groupList, 10);
        for (List<Entity> batch : batchList) {
            service.updateAndSaveBatch(
                groupId, batch, dataMap, userName);
        }
    });
    
    return Result.success("处理完成");
}

@Transactional(rollbackFor = Exception.class)
public void updateAndSaveBatch(String groupId, List<Entity> batch,
        Map<String, RelatedData> dataMap, String userName) {
    // 更新关联数据
    List<String> idList = batch.stream()
        .map(Entity::getId)
        .collect(Collectors.toList());
    UpdateRequest req = new UpdateRequest();
    req.setGroupId(groupId);
    req.setIdList(idList);
    updateRelatedData(req, userName);
    
    // 构建批量插入数据
    List<TargetEntity> targetList = batch.stream().map(e -> {
        RelatedData data = dataMap.get(e.getId());
        if (data == null) {
            log.warn("关联数据不存在,跳过,id: {}", e.getId());
            return null;
        }
        return new TargetEntity()
            .setId(e.getId())
            .setName(data.getName())
            .setStatus(StatusEnum.ACTIVE)
            .setCreateTime(new Date())
            .setField1(e.getField1())
            .setField2(e.getField2());
    }).filter(Objects::nonNull)
      .distinct()
      .collect(Collectors.toList());
    
    // 批量插入
    if (!CollectionUtils.isEmpty(targetList)) {
        targetService.saveBatch(targetList);
        log.info("批量插入成功,分组: {},本批{}条", groupId, targetList.size());
    }
}

该方案既保证了数据一致性,又优化了性能和并发能力。

相关推荐
松涛和鸣2 小时前
72、IMX6ULL驱动实战:设备树(DTS/DTB)+ GPIO子系统+Platform总线
linux·服务器·arm开发·数据库·单片机
likangbinlxa3 小时前
【Oracle11g SQL详解】UPDATE 和 DELETE 操作的正确使用
数据库·sql
r i c k3 小时前
数据库系统学习笔记
数据库·笔记·学习
野犬寒鸦3 小时前
从零起步学习JVM || 第一章:类加载器与双亲委派机制模型详解
java·jvm·数据库·后端·学习
IvorySQL4 小时前
PostgreSQL 分区表的 ALTER TABLE 语句执行机制解析
数据库·postgresql·开源
·云扬·4 小时前
MySQL 8.0 Redo Log 归档与禁用实战指南
android·数据库·mysql
IT邦德4 小时前
Oracle 26ai DataGuard 搭建(RAC到单机)
数据库·oracle
惊讶的猫5 小时前
redis分片集群
数据库·redis·缓存·分片集群·海量数据存储·高并发写
不爱缺氧i5 小时前
完全卸载MariaDB
数据库·mariadb
纤纡.5 小时前
Linux中SQL 从基础到进阶:五大分类详解与表结构操作(ALTER/DROP)全攻略
linux·数据库·sql