在CompletableFuture中用CrudRepository保存Oracle数据失败排查
兄弟,你的推测完全命中了——这就是多线程环境下Spring事务的坑!我之前做异步导出+入库的时候也踩过一模一样的坑,给你拆解下原因和解决办法:
问题根源
Spring的事务管理是基于ThreadLocal实现的,也就是说事务上下文只绑定在当前线程上。你用CompletableFuture.supplyAsync()的时候,默认会用JDK的ForkJoinPool里的线程来执行任务,这些线程和发起请求的主线程不是同一个,自然拿不到原来的事务上下文。而CrudRepository的save方法如果没有在事务环境下执行,默认是不会自动提交的,这就导致你看到实体数据正确,但数据库里没东西的情况。
几个可行的解决方案
1. 给入库的服务方法加上@Transactional(最直接)
首先确保你调用的save方法所在的服务层方法是被Spring事务管理的,而且要注意必须通过Spring代理调用这个方法(不能在同一个类里直接调用,否则代理不生效,事务注解白加)。
比如你的数据服务类应该是这样:
@Service public class DataSaveService { private final YourCrudRepository crudRepository; public DataSaveService(YourCrudRepository crudRepository) { this.crudRepository = crudRepository; } @Transactional(rollbackFor = Exception.class) // 明确开启事务,指定回滚异常 public void batchSave(List<YourEntity> entities) { crudRepository.saveAll(entities); // 这里如果有异常,事务会自动回滚 } }
然后在excelExportService.exportExcel里,通过Spring注入的DataSaveService实例调用batchSave,而不是直接调用仓库的save方法。
2. 用Spring的@Async代替手动CompletableFuture(更规范)
如果你的异步逻辑比较复杂,推荐用Spring提供的@Async注解来管理异步任务,这样Spring会自动帮你处理线程上下文和事务的传播。
步骤如下:
- 在配置类上加
@EnableAsync开启异步支持 - 把异步逻辑抽成单独的方法,加上
@Async注解,同时确保这个方法所在的类是Spring bean - 如果需要事务,在入库方法上依然加
@Transactional
示例:
@Service public class AsyncExportService { private final Caller caller; private final ExcelExportService excelExportService; public AsyncExportService(Caller caller, ExcelExportService excelExportService) { this.caller = caller; this.excelExportService = excelExportService; } @Async public CompletableFuture<byte[]> exportAndSave(**some parameters**) { List<Data> dataList = caller.getForObjectList(**some parameters**); Collections.sort(**some sorting**); byte[] excelBytes = excelExportService.exportExcel(dataList, **some parameters**); return CompletableFuture.completedFuture(excelBytes); } }
然后调用的时候直接注入AsyncExportService调用方法就行,Spring会自动用它管理的线程池执行,事务也能正常工作。
3. 自定义Spring管理的线程池(适合必须用CompletableFuture的场景)
如果你一定要手动用CompletableFuture,可以自己配置一个被Spring管理的线程池,这样线程会继承Spring的上下文,事务注解就能生效。
先配置线程池:
@Configuration public class AsyncConfig { @Bean public TaskExecutor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(20); executor.setThreadNamePrefix("Export-Thread-"); executor.initialize(); return executor; } }
然后在你的方法里用这个线程池:
@Autowired private TaskExecutor taskExecutor; public CompletableFuture<byte[]> getCompletableFuture(**some parameters**) { return CompletableFuture.supplyAsync(() -> caller.getForObjectList(**some parameters**), taskExecutor) .thenApply(listWithData-> { Collections.sort(**some sorting**); return excelExportService.exportExcel(listWithData,**some parameters**); }); }
这样异步任务用的是Spring管理的线程,能拿到事务上下文,入库操作就能正常提交了。
额外提醒
你用.get()来获取CompletableFuture的结果会阻塞主线程,要是你的场景允许,最好用.thenAccept()或者异步回调的方式处理结果,避免阻塞。不过如果是同步等待结果的场景,那也没问题,记得处理InterruptedException和ExecutionException哦。
内容的提问来源于stack exchange,提问作者Lucian

