使用JpaRepository并行保存

时间:2019-07-10 23:39:53

标签: java spring spring-data-jpa spring-data database-concurrency

我正在开发一个服务于REST端点的微服务,该服务使用spring数据将数据保存到数据库或从数据库检索数据。

让我们将实体类Foo的ID字段称为简单的Long和其他数据字段。每个Foo的ID不会在此服务中自动生成,它们是从知道使其唯一的外部来源提供的。

该服务具有一个POST端点,该端点同时提供CRUD模型的创建和更新功能,该调用在代码的服务层中调用相应的功能,我们将此功能称为AddData(Foo foo_incoming)。 POST消息的正文包含要保存到数据库的数据和要保存数据的Foo的ID。 AddData的逻辑如下:

@Service("fooService")
public class FooServiceImpl {

    @Autowired
    FooRepository fooRepository; // Subinterface of JpaRepository

    @Transactional
    public Long AddData(Foo foo_incoming) {
        Optional<Foo> foo_check = fooRepository.findById(incoming.getId());
        Foo foo_exists;

        // Exists already?
        if (foo_check.isEmpty()) {
            // New Foo
            foo_exists = fooRepository.saveAndFlush(foo_incoming);
        } else {
            // Update existing foo
            foo_exists = foo_check.get();
            foo_exists.addToFieldA(foo_incoming.getFieldA());
            foo_exists.addToFieldB(foo_incoming.getFieldB());
        }

        return foo_exists.getId();
    }

}

此功能负责创建Foo的初始记录和更新记录。

当POST请求传入将数据添加到ID = 1的某些Foo时,我们称其为foo-1,如果它们之间有一段合理的时间间隔,则不存在此请求它们,第一个请求将为foo-1创建初始记录,而所有后续调用将仅更新。即saveAndFlush需要足够的时间才能实际刷新到数据库,因此对findById的后续调用会在数据库中找到foo-1,然后跳转到else块并仅更新其字段

我遇到的问题是,当相同的Foo(相同ID)的N个POST足够快地发送到服务时,似乎对AddData的所有相应调用都同时发生。因此,如果foo-1还不存在,则在对AddData的每个调用中,findById(1)返回空。因此saveAndFlush被ID = 1的Foo调用了N次,这引发了DataIntegrityViolationException

为了解决这个问题,我已经在网上闲逛了几天。

  • 该方法已被注释@Transactional。我尝试仅在方法和整个类上使用@Transactional(isolation = Isolation.SERIALIZABLE),但无济于事。
  • 我尝试用findByIdsaveAndFlush分别注释FooRepository中的@Lock(LockModeType.PESSIMISTIC_READ)@Lock(LockModeType.PESSIMISTIC_WRITE)方法,但是没有运气。
  • 我尝试将@Version字段添加到Foo,保持不变。

我不知道如何强制AddData依次发生,我认为这就是@Transactional(isolation = Isolation.SERIALIZABLE)应该做的事情。

我正在考虑赋予“创建”和“更新”它们自己的功能-创建用于创建的PUT端点。但是,然后PUT端点将有一个类似的问题-如果我想尝试防止代码中的主键冲突,那么在执行findById之前,我必须对saveAndFlush做类似的检查。但是,实际使用此服务的方式可能无法选择PUT端点。

saveAndFlush包装在try / catch块中确实捕获异常,令我惊讶的是。我可以尝试一些时髦的逻辑来尝试在findById失败时再次调用saveAndFlush,但是如果有一种方法可以避免引发异常,则我更愿意这样做。

任何建议将不胜感激!

编辑:更多有用的上下文。该微服务在Kubernetes集群中运行,在该集群中,可能有许多该服务实例同时服务于请求。我仍在研究处理多个实例的并发性,并弄清我不必自己做—我的团队正在开发像这样的几个微服务,我们可能会开发一个通用库来解决所有这些问题

编辑2:到目前为止,我已经忘记了,我在运行服务时使用的是H2数据库,而不是真正的数据库。可能与此有关吗?

我要重申,这里发生的是在foo-1存在之前,多次调用以检查foo-1数据库;因此,我认为数据库锁定不会对我有帮助,因为没有实体锁定。我认为强制AddData依次执行将解决此问题,而我对为什么将@Transactional(isolation = Isolation.SERIALIZABLE)添加到AddData却不为我感到困惑。

2 个答案:

答案 0 :(得分:0)

有一些方法可以以有益的方式与Jpa一起使用并发,但是总的来说,不可能同时进行Jpa调用。

请记住,Jpa依赖于EntityManager, Session, Connection, etc.这样的类对象,它们不是线程安全的。它们的设计旨在避免竞争状况,死锁以及多线程可能引起的所有问题。话虽这么说,Jpa需要阻止数据库调用。

尽管如此,您肯定可以与JPA方法同时实现业务逻辑,以提高性能,这似乎已经知道。.我经常使用池/执行器,但仍然找到理由偏爱Jpa而不是替代方法。在许多情况下,完成Jpa操作所需的时间与进行数据创建,验证等所需的时间相比非常短。尽管如此,仍需要做出一些折衷,因为最终将需要多线程上下文中的每个线程在Jpa事件循环上进行阻塞调用。据我所知,Isolation.SERIALIZABLE似乎已经是您追求目标的最重要步骤。

您可能想研究R2dbc,它是JDBC的一种反应性实现,可以帮助您完成此处要尝试的操作。它已经开发了很长一段时间,并且即将发布。最后我听说应该在十月份完成,而我的团队已经开始在单独的分支机构进行转换。

答案 1 :(得分:-1)

我不确定如何使用@Transactional来实现,但是还有另一种使用同步块来解决问题的方法。如您所言,您的密钥将是唯一的,根据该密钥您将发现if对象是否已存在,我建议将其用作同步插入/更新块的密钥。

当您的键匹配时,我正在使用String实习生返回相同的对象。

@Service("fooService")
public class FooServiceImpl {

    @Autowired
    FooRepository fooRepository; // Subinterface of JpaRepository

    @Transactional
    public Long AddData(Foo foo_incoming) {
        Optional<Foo> foo_check = fooRepository.findById(incoming.getId());
        String key = **incoming.getId().intern();**
        Foo foo_exists;
        **synchronized (key)** {
            // Exists already?
            if (foo_check.isEmpty()) {
                // New Foo
                foo_exists = fooRepository.saveAndFlush(foo_incoming);
            } else {
                // Update existing foo
                foo_exists = foo_check.get();
                foo_exists.addToFieldA(foo_incoming.getFieldA());
                foo_exists.addToFieldB(foo_incoming.getFieldB());
            }
        }

        return foo_exists.getId();
    }

}

这篇文章可能会帮助您进一步。 Synchronizing on String objects in Java