阻止两个事务创建同一实体,但仍允许并发创建

时间:2019-04-17 18:44:34

标签: spring multithreading jpa spring-transactions transaction-isolation

我有一个与第三方系统连接以创建和存储汽车数据的系统。用户选择一个try并使用它在我的系统中创建一个ThirdPartyCar

一种维修方法可以节省汽车。但是,只有在其他人尚未尝试使用该Car来保存汽车时,它才应该保存:

ThirdPatyCar

以下是导致问题的情况:

@Transactional(transactionManager="mySystemTransactionManager", isolation=Isolation.?)
public void saveNewCarAndMapToThirdPartyCar(Car car, Long thirdPartyCarId) {

     // Mapping table tracks which ThirdPartyCar was used to create my Car. 
     // The thirdPartyCarId is the primary key of the table.
     if (!thirdPartyCarMapRepo.existsById(thirdPartyCarId)) {

            // sleep to help test concurrency issues
            log.debug("sleep");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("awake");

            Car car = carRepository.save(car);
            thirdPartyCarMapRepo.save(new ThirdPartyCarMap(thirdPartyCarId, car));
    }
}

将隔离设置为低于Isolation.SERIALIZABLE的任何值,这两个用户似乎都将具有existById返回false。这样就创建了两辆汽车,只有最后保存的一辆被映射回第三方汽车。

如果我设置了Isolation.SERIALIZABLE,则即使来自不同的第三方汽车,用户也无法同时创建汽车。

当第三方汽车与用户A相同时,如何阻止用户B创建汽车,但是当第三方汽车与用户A不同时,仍然允许两者同时创建?

更新

经过更多思考和研究之后,我相信这可能不是事务隔离问题。

这是ThirdPartyCarMap表:

User A                        User B
 existsById                      |
    |                            |
    |                         existsById
    |                            |
    |                            |
 carRepo.save                    |
 thirdPartyCarMapRepo.save       |
    |                            |
    |                            |
    |                          carRepo.save
    |                          thirdPartyCarMapRepo.save

以上面的方案图为例:

用户A thirdPartyCarId (PK, bigint, not null) carId (FK, bigint, not null) /* reference my system's car table */ 这样做:

thirdPartyCarMapRepo.save

但是,用户B inserts into ThirdPartyCarMap (thirdPartyCarId, carId) values (45,1) 确实:

thirdPartyCarMapRepo.save

因此,导致问题的原因是保存调用的双重性质。我认为我有以下可能的解决方案:

  • 方法1:实现持久化并覆盖here
  • 中所述的isNew行为
  • 方法2:向执行插入操作的存储库添加本机查询
  • 方法3:添加代理主键(例如自动递增ID),并删除update ThirdPartyCarMap set carId =2 where thirdPartyCarId=45 作为主键,但使其唯一。

我认为最后一个选项可能是最好的,但是每个选项都有问题:  -方法1:即使读取数据,isNew也会返回true  -方法2:看起来有点像骇客  -方法3:似乎是为了JPA而将额外的数据添加到表中。

还有没有这些问题的更好的方法吗?

2 个答案:

答案 0 :(得分:0)

一种选择是在数据库级别上使用唯一约束,这可以保证您不能拥有两个具有相同VIN的PhysicalCar(如果这是您在PhysicalCar数据库中的唯一标识)。这意味着在您的情况下,当两个线程将尝试添加PhysicalCar时,其中一个将因唯一约束冲突而失败。 Spring已经将数据库异常包装到DataIntegrityViolationException中,但是您可以从中提取约束名称,如果您希望在PhysicalCar重复的情况下抛出该约束名称,则可以对其执行操作。

答案 1 :(得分:0)

您可以使用ThirdPartyCar或MD5创建用户A和用户B的CarhashCode()对象的校验和,并检查是否相等。如果您发现用户B的汽车与用户A的ThirdPartyCar相同,则抛出RuntimeException,否则继续。