Spring Boot异步方法批量存PostgreSQL,如何实时展示已保存数据?
问题背景
我写了一个带@Async注解的异步方法,用来读取CSV文件并把每一行数据存入PostgreSQL。但发现用户触发这个流程后,去列表页调用repository.findAll()查询时,必须等整个异步方法执行完,所有记录才会显示出来。我本来以为是Spring要等方法返回才释放数据库连接,试过调用myRepo.flush()但没效果。想请教下有没有办法让每条记录保存后就立刻能被查询到?
问题根源:不是连接,是事务!
你猜的连接释放其实不对,真正的问题是事务边界。Spring的@Async方法默认会开启一个方法级的事务——整个循环都包裹在同一个事务里,只有当整个方法执行完、事务提交后,所有变更才会对其他查询可见。
至于flush()为什么没用?它只是把Hibernate持久化上下文里的变更同步到数据库(也就是把INSERT语句发给数据库),但此时事务还没提交,数据库会把这些变更留在当前事务的私有上下文里。PostgreSQL默认的隔离级别是读已提交(Read Committed),其他事务(比如你列表页的findAll())看不到未提交的变更,所以就算flush了也没用。
解决方案:让每条记录的保存都在独立事务里
要实现实时可见,核心就是让每条记录的保存操作都在单独的事务中执行,执行完就提交,这样每条数据一保存就能被其他查询看到。这里有两种靠谱的实现方式:
1. 拆分出独立事务的保存方法(推荐)
单独写一个带@Transactional注解的方法,专门负责保存单条记录,然后在异步方法里循环调用这个方法。每个@Transactional方法会触发独立的事务,执行完毕就自动提交。
注意:因为Spring的事务是基于代理的,同一个类内的方法调用不会触发事务代理,所以要么把这个保存方法放到另一个Bean里,要么在当前类中注入自己(通过Spring容器获取代理对象)。
代码示例:
// 假设这是你的服务类 @Service public class MyRecordService { @Autowired private MyRecordRepo myRepo; // 注入自己,用来触发事务代理 @Autowired private MyRecordService self; @Async public void processCsvRecords(List<MyRecordCsv> records) { for(MyRecordCsv record : records) { // 调用带独立事务的方法 self.saveSingleRecord(record); } } // 每个调用都会开启并提交独立事务 @Transactional public void saveSingleRecord(MyRecordCsv record) { myRepo.save(record); // 这里不需要手动flush,事务提交时会自动同步 } }
2. 用TransactionTemplate手动控制事务
如果不想拆分方法,也可以用TransactionTemplate来手动为每条记录的保存创建独立事务,这种方式不需要依赖方法级的事务代理:
@Service public class MyRecordService { @Autowired private MyRecordRepo myRepo; @Autowired private TransactionTemplate transactionTemplate; @Async public void processCsvRecords(List<MyRecordCsv> records) { for(MyRecordCsv record : records) { // 每次execute都会开启新事务,执行完自动提交 transactionTemplate.execute(status -> { myRepo.save(record); return null; }); } } }
额外提醒
这种每条记录单独提交的方式,好处是数据实时可见,但也要考虑到性能——如果CSV有上万条记录,频繁的事务提交会带来一定的性能开销。如果你的场景对实时性要求没那么高,也可以考虑每N条记录提交一次事务,平衡实时性和性能。
内容的提问来源于stack exchange,提问作者Fabio Ebner

