我有两台服务器连接到同一个数据库。两者都有预定的作业,我并不关心哪一个运行预定的作业,只要只有一个运行即可。因此,我们的想法是在数据库中保留一个键值对,并且无论哪个读取值为 0,第一个都可以运行计划的作业。
理想情况下,它会像这样工作:
- 应用 A 和应用 B 同时运行计划作业。
- 应用A首先访问数据库,锁定表的读写。
- 应用 A 将值设置为 1 并释放锁定。
- 应用 A 开始执行预定作业。
- 应用 B 从其数据库请求中读取值 1,并且不运行计划作业。
我有一个配置表,用于保存锁的状态。
config:
name: VARCHAR(55)
value: VARCHAR(55)
存储库:
@Repository
public interface ConfigRepository extends CrudRepository<Config, Long> {
@Lock(LockModeType.PESSIMISTIC_READ)
Config findOneByName(String name);
@Lock(LockModeType.PESSIMISTIC_WRITE)
<S extends Config> S save(S entity);
}
服务:
@Service
public class ConfigService {
@Transactional
public void unlock(ConfigEnum lockable) {
Config lock = configRepository.findOneByName(lockable.getSetting());
lock.setValue("0");
configRepository.save(lock);
}
@Transactional
public void lock(ConfigEnum lockable) {
Config lock = configRepository.findOneByName(lockable.getSetting());
lock.setValue("1");
configRepository.save(lock);
}
@Transactional
public boolean isLocked(ConfigEnum lockable) {
Config lock = configRepository.findOneByName(lockable.getSetting());
return lock.getValue().equals("1");
}
}
调度程序:
@Component
public class JobScheduler {
@Async
@Scheduled("0 0 1 * * *")
@Transactional
public void run() {
if (!configService.isLocked(ConfigEnum.CNF_JOB.getJobName())) {
configService.lock(ConfigEnum.CNF_JOB.getJobName());
jobService.run();
configService.unlock(ConfigEnum.CNF_JOB.getJobName());
}
}
}
但是我注意到计划的作业仍然在两个应用程序上同时运行。有时会引发死锁,但如果遇到死锁,Spring 似乎会重试事务。此时,一个应用程序似乎已完成,因此该应用程序再次开始相同的工作(不确定)。
任务并不短,可以建立锁、更新表、运行任务和释放锁。我希望保持这个非常简单,而不涉及 Quartz 或 ShedLock 等其他库。
最佳答案
我认为您的交易时间太短。您不会在 run 方法中启动事务,但每个 ConfigService 方法都是事务性的。最有可能的是,每个方法都会获得一个新事务并在完成后提交。提交会释放锁,因此 isLocked 和锁之间存在竞争条件。
结合isLocked和lock:
@Transactional
public boolean tryLock(ConfigEnum lockable) {
Config lock = configRepository.findOneByName(lockable.getSetting());
if("1".equals(lock.getValue()) {
return false;
}
lock.setValue("1");
configRepository.save(lock);
return true;
}
这会在同一事务中进行检查和写入,并且应该可以工作。
顺便说一句,这是一种危险的方法。如果拥有锁的节点死亡会发生什么?有很多可能的解决方案。一种是锁定特定记录并在整个作业过程中保持该锁定。另一个节点无法继续,如果第一个节点死亡,锁将被释放。另一种是使用时间戳而不是 1,并要求所有者定期更新时间戳。或者你可以引入像 Zookeeper 这样的东西。
关于java - Spring :PESSIMISTIC_READ/WRITE 不起作用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50899370/