我有一个基于计时器的工作
@Component
public class Worker {
@Scheduled(fixedDelay = 100)
public void processEnvironmentActions() {
Job job = pickJob();
}
public Job pickJob() {
Job job = jobRepository.findFirstByStatus(Status.NOT_PROCESSED);
job.setStatus(Status.PROCESSING);
jobRepository.save(job);
return job;
}
}
现在,在大多数情况下,这应该给我正确的结果。但是,如果有两个微服务实例同时执行这段代码会发生什么?
我如何确保即使有多个服务实例,存储库也应始终仅将一个作业分配给一个实例,而不应分配给其他实例。
编辑:
我认为人们对@Transactional
感到困惑/集中,因此将其删除。问题仍然相同。
答案 0 :(得分:1)
我对spring-batch不熟悉,但是显然spring-batch实现了乐观锁定,因此如果另一个线程已经选择了相同的作业,则保存操作将失败。
答案 1 :(得分:1)
但是,如果有两个微服务实例同时执行这段代码会发生什么?
答案通常是:这取决于。
所有这些都假定您的代码在事务内运行
乐观锁定。
如果您的Job
实体没有版本属性,即带有@Version
注释的属性。
启用了乐观锁定。
如果要访问同一作业的进程,则在尝试保留已更改的作业实体并失败并显示OptimisticLockingException
时,将会注意到版本属性已更改。
您所需要做的就是处理该异常,这样您的处理就不会死亡,但是会再次尝试获取下一个Job
。
没有(JPA级别)锁定。
如果Job
实体没有版本属性,则默认情况下,JPA将不应用任何锁定。
访问Job
的第二个进程将发出一个更新,本质上是一个NOOP,因为第一个进程已经对其进行了更新。
两者都不会注意到问题。
您可能要避免这种情况。
悲观锁定
pessimistic_write锁将阻止任何人在完成读写实体之前读取实体(至少这是我对JPA规范的理解)。 因此,这应避免第二个过程能够在第一个过程完成写入之前选择该行。 这可能会阻塞整个第二个过程。 因此,请确保持有这种锁的交易短。
为了获得这种锁,请用findFirstByStatus
注释存储库方法@Lock(LockModeType.PESSIMISTIC_WRITE)
。
当然,可能有一些库或框架可以为您处理这类细节。
答案 2 :(得分:0)
我同意canvasvg的回答,但是在不了解刷新策略的情况下,应使用jobRepository.saveAndFlush(job)
方法,以确保将sql语句下推到数据库。
另请参阅@Conffusion
答案 3 :(得分:0)
@Jens Schauder的回答向我指出了正确的方向。让我共享代码,以指导其他人。这就是我解决问题的方式,我如下更改了工作类别
@Entity
public class Job {
@Version
private Long version = null;
// other fields omitted for bervity
}
现在,让我们跟踪以下代码
@Transactional
public Job pickJob() {
Job job = jobRepository.findFirstByStatus(Status.NOT_PROCESSED);
job.setStatus(Status.PROCESSING);
Job saved jobRepository.save(job);
return saved;
}
注意 :确保返回
saved
对象,而不是job
对象。如果您返回作业对象,它将在第二save
天内失败 操作,因为用于job
的版本计数将落后于saved
的内容。
。
Service 1 Service 2 1. Read Object (version = 1) 1. Read Object (version = 1) 2. Change the object and save (changes the version) 3. Continues to process 2. Change the object and save (this operation fails as the version that was read was 1 but in the DB version is 2) 3. Skip the job processing
这样,仅需一个过程即可完成作业。