【问题标题】:Prevent two transactions from creating the same entity but still allow concurrent creation防止两个事务创建同一个实体,但仍允许并发创建
【发布时间】:2019-04-17 18:44:33
【问题描述】:

我有一个与第三方系统交互以创建和存储汽车数据的系统。用户选择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));
    }
}

这是导致问题的场景:

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

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

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

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

更新

在对此进行了更多思考和研究之后,我相信这可能不是事务隔离问题。

这是 ThirdPartyCarMap 表:

thirdPartyCarId (PK, bigint, not null)
carId (FK, bigint, not null)  /* reference my system's car table */

以上面的场景图为例:

用户 A thirdPartyCarMapRepo.save 这样做:

inserts into ThirdPartyCarMap (thirdPartyCarId, carId) values (45,1)

但是,用户 B thirdPartyCarMapRepo.save 会:

update ThirdPartyCarMap set carId =2 where thirdPartyCarId=45

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

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

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

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

【问题讨论】:

  • 您是否尝试将身份添加到ThirdPartyCarMap,并使用空值保存新实例?
  • @yegodm,是的,我的 id 为thirdPartyCarId,但问题是 PK 在保存 b/c 时不为空,它始终具有来自第三方系统的值。因此,我尝试用独立于第三方系统的 PK 替换(即自动递增 id)。请参阅更新后的帖子,了解我对这种方法和更新问题的看法。谢谢。
  • 哦,抱歉,我没有仔细阅读方法#3。这基本上就是我的意思。然而,我仍然会选择那个,而不是担心额外的数据。从另一个角度来看,如果我没记错的话,还有一种可能的方法。这两个 id 可以构成一个复合键,例如 Key(thirdPartyCarId:Long, carId:Long) 映射到带有 @EmbeddedId 注释的字段 key: Key
  • 不用担心。感谢您提供的附加选项,它可以避免数据库中的额外列,但我想可能会添加更多代码。我倾向于方法 3。

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


【解决方案1】:

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

【讨论】:

  • 感谢您的回答。您是否建议我在映射表中添加另一个唯一列?因此,映射表将具有以下列:thirdPartyCarId - PKcarId - FKVIN(或独特的东西)- unique constraint。我认为问题在于用户 B 的保存只会简单地更新用户 A 通过其保存操作插入的映射记录。
  • 由于 spring 正在检查 '@Id' 属性以确定它是否应该进行更新或插入,我将在该表中拥有一个增量 '@Id' 以及 ThirdPartyCarId 的唯一约束.这样第二次创建将不会传递任何 Id,spring 会将其视为插入(新记录)并且您的 DataConstraintViolation 将被抛出,然后您必须根据需要对其进行操作(将错误返回给用户等)
  • 啊,我明白了。这是我对 OP 的更新中的方法 3。除非有更好的选择,否则我就是这样做的。
【解决方案2】:

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

【讨论】:

    猜你喜欢
    • 2020-10-10
    • 1970-01-01
    • 2012-08-25
    • 1970-01-01
    • 2020-09-12
    • 2011-10-08
    • 2017-03-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多