我正在编写一个应用程序,该应用程序定期从数据库表中提取新行(row.status =='NEW'),对每行作为JPA实体进行一些处理,然后以状态==将该行保存回数据库“已处理”。
数据库表:
ID | Status
1 | PROCESSED
2 | NEW
3 | NEW
Java代码:(使用Spring引导框架)
@Component
public class Processor {
// an JPA repository for selecting Items
@Autowired
ItemRepository itemRepository;
// a thread executor for submitting
ExecutorService executor = Executors.newSingleThreadExecutor();
@Scheduled(fixed-rate=1000)
void process() {
List<Item> newItems = itemRepository.findByStatus('NEW');
for(Item item : newItems) {
// process each item asyncronously
executor.submit(()-> {
// do some processing on this item and update status.
// THis is time consuming process, may take 4 or 5 seconds
item.setStatus("PROCESSED");
itemRepository.save(item);
});
}
}
}
问题是,当一项item1
仍在executor
中处理,并且状态未更新为PROCESSED
时,在下一轮处理中,它仍然将由itemRepository.findByStatus('NEW')
选择。它将再次提交进行处理。
如何避免这种情况发生? (除了将fixed-rate
更改为fixed-delay
之外,是否存在诸如syncronize (item) { .... }
之类的锁定机制,使得一旦数据库行仍在处理中,就不会在下一轮中再次选择它。 process()
方法?
答案 0 :(得分:0)
我认为使用Spring调度程序不容易做到这一点。另外,如果您可以在同一个JVM中找到具有某些同步功能的单实例解决方案,那么如果在具有不同JVM的集群中运行多个实例,这将失败。
您可以移至Quartz,后者可以使用(JDBC)数据库来允许一次仅执行一个作业实例。实施org.springframework.scheduling.quartz.QuartzJobBean
并将其添加到Spring设置中。
搜索spring boot 2 Quartz
的设置方法。这里会占用太多空间,但并不困难。一个开始可能是Spring documentation。
答案 1 :(得分:0)
您是否考虑过将自己的状态设置为第三种状态?即PROCESSING
-这是一种确保没有2个线程试图处理同一项目的简单方法,每个线程只能处理NEW
个工作。
除了使用的STATUS
对象只是一个字符串字段外,我已经做了类似的事情。要保留工作,它将变成UPDATE TOP 1 FROM table set status = status + :randomString WHERE status = 'NEW'
,然后重新选择开始工作。
答案 2 :(得分:0)
您需要一个簿记数据结构来跟踪已提交给执行者的任务。您可以在Item
实体中引入一个新状态来进行跟踪,但是考虑到调度频率和项目数量,该方法将引入很多数据库行程,这可能会影响性能。
通过将ConcurrentHashMap
的ID放入地图中,使用Items
来跟踪已提交给执行者的Item
。保存Item
后,从地图上删除Item
的ID。此映射将帮助您快速决定是否将Item
提交给执行者。
答案 3 :(得分:0)
在我看来,可以通过使用@Transactional
隔离级别的未提交读取来解决此问题。请参考以下问题:
Spring @Transactional - isolation, propagation
附加说明是在其他线程当前正在处理的项目上添加ON_PROCESS的另一状态作为标志,该状态将在执行处理之前保存。如果抛出异常,则它将自动回滚,但是成功将导致将其保存到PROCESSED。这里的关键是,只要状态不是NEW,那么只要您正在读取未提交的状态,它就不会被您的计划任务所接管。