问题背景
在批量导入功能中,发现 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 倍,说明:
saveBatch确实执行了- 记录确实插入了
- 但查询时包含了旧记录
进一步分析发现:虽然记录已插入,但可能因为事务未提交,导致查询时看不到新数据。
根本原因
问题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())
总结
- 问题根源:异步线程中事务未正确提交
- 解决方案:使用
@Transactional+ 接口代理调用,确保事务生效 - 性能优化:缩小事务范围,分批处理,批量查询
- 最佳实践:事务只用于关键数据操作,保持事务范围最小
通过以上优化,既解决了数据插入问题,又提升了系统性能和并发能力。
参考代码
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());
}
}
该方案既保证了数据一致性,又优化了性能和并发能力。