Spring:PESSIMISTIC_READ/WRITE 不工作
Spring: PESSIMISTIC_READ/WRITE not working
我有两台服务器连接到同一个数据库。两者都有预定的工作,我真的不在乎哪一个 运行 是预定的工作,只要只有一个。因此,我们的想法是在数据库中保留一个键值对,无论哪个读取值为 0 的值首先到达 运行 计划的作业。
理想情况下,它会这样工作:
- 应用程序 A 和应用程序 B 运行 同时安排作业。
- 应用A先访问数据库,锁定table读写。
- 应用程序A将值设置为1并释放锁。
- 应用程序 A 开始处理计划的作业。
- 应用程序 B 从它的数据库请求中读取值 1,而不是 运行 计划的作业。
我有一个配置 table,我可以在其中保持锁状态。
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 似乎会在遇到死锁时重试事务。那时似乎一个应用程序已经完成,所以这个应用程序再次开始相同的工作(不确定)。
任务并不短,可以建立锁、table 更新、任务 运行 和释放锁。我想保持这个非常简单,而不涉及其他库,如 Quartz 或 ShedLock。
我认为您的交易时间太短了。您不会在 运行 方法中启动事务,但每个 ConfigService 方法都是事务性的。很可能每种方法都会获得一个新事务并在完成后提交。 commit 会释放锁,所以 isLocked 和 lock 之间存在竞争条件。
合并 isLocked 并锁定:
@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 这样的东西。
我有两台服务器连接到同一个数据库。两者都有预定的工作,我真的不在乎哪一个 运行 是预定的工作,只要只有一个。因此,我们的想法是在数据库中保留一个键值对,无论哪个读取值为 0 的值首先到达 运行 计划的作业。
理想情况下,它会这样工作:
- 应用程序 A 和应用程序 B 运行 同时安排作业。
- 应用A先访问数据库,锁定table读写。
- 应用程序A将值设置为1并释放锁。
- 应用程序 A 开始处理计划的作业。
- 应用程序 B 从它的数据库请求中读取值 1,而不是 运行 计划的作业。
我有一个配置 table,我可以在其中保持锁状态。
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 似乎会在遇到死锁时重试事务。那时似乎一个应用程序已经完成,所以这个应用程序再次开始相同的工作(不确定)。
任务并不短,可以建立锁、table 更新、任务 运行 和释放锁。我想保持这个非常简单,而不涉及其他库,如 Quartz 或 ShedLock。
我认为您的交易时间太短了。您不会在 运行 方法中启动事务,但每个 ConfigService 方法都是事务性的。很可能每种方法都会获得一个新事务并在完成后提交。 commit 会释放锁,所以 isLocked 和 lock 之间存在竞争条件。
合并 isLocked 并锁定:
@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 这样的东西。