我正在开发一个服务于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)
,但无济于事。findById
和saveAndFlush
分别注释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
却不为我感到困惑。
答案 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