【发布时间】:2019-07-10 23:39:53
【问题描述】:
我正在开发一个微服务,它为 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,它还不存在,如果它们之间有合理的时间,第一个request 将为foo-1 创建初始记录,所有后续调用只会更新。 IE。 saveAndFlush 有足够的时间实际刷新到数据库,因此对 findById 的后续调用会在数据库中找到 foo-1,然后跳转到 else 块并仅更新其字段。
我遇到的问题是,当相同Foo(相同ID)的N POST 足够快地发送到服务时,似乎所有对AddData 的相应调用同时发生。因此,如果foo-1 尚不存在,则在对AddData 的每个调用中,findById(1) 将返回空。因此,saveAndFlush 在 ID=1 的 Foos 中被调用 N 次,从而引发了 DataIntegrityViolationException。
我已经在网上搜索了几天试图解决这个问题。
- 该方法已注释
@Transactional。我试过在方法和整个类上使用@Transactional(isolation = Isolation.SERIALIZABLE),但没有帮助。 - 我试过用
@Lock(LockModeType.PESSIMISTIC_READ)和@Lock(LockModeType.PESSIMISTIC_WRITE)分别注释FooRepository中的findById和saveAndFlush方法,但没有成功。 - 我尝试将
@Version字段添加到Foo,没有任何变化。
我不知道如何强制AddData 连续发生,我认为这是@Transactional(isolation = Isolation.SERIALIZABLE) 应该做的。
我正在考虑赋予“创建”和“更新”它们自己的功能 - 为创建创建一个 PUT 端点。但是 PUT 端点也会有类似的问题——如果我想尝试防止代码中的主键冲突,我必须在执行saveAndFlush 之前对findById 进行类似的检查。但是这个服务的实际使用方式,PUT端点可能不是一个选项。
将saveAndFlush 包装在try/catch 块中确实 捕获异常,令我惊讶。当saveAndFlush 失败时,我可以尝试一些时髦的逻辑来尝试再次调用findById,但如果有办法避免抛出异常,我更愿意这样做。
任何建议将不胜感激!
编辑:更多可能有用的上下文。此微服务在 Kubernetes 集群中运行,其中可能有许多此服务实例同时服务请求。我仍在研究处理多个实例的并发性,并弄清楚这不是我必须自己做的事情 - 我的团队正在开发几个这样的微服务,我们可能会开发一个通用库来解决所有这些问题.
编辑 2:我现在忘记了,我在运行服务时使用的是 H2 数据库,而不是真正的数据库。会不会跟这个有关系?
我要重申,这里发生的情况是多次调用检查数据库中是否存在 foo-1,在 foo-1 存在之前;因此,我认为数据库锁定在这里不会对我有帮助,因为没有实体 to 锁定。我认为强制AddData 连续发生可以解决这个问题,我完全不知道为什么将@Transactional(isolation = Isolation.SERIALIZABLE) 添加到AddData 对我没有这样做。
【问题讨论】:
-
我无法理解您的用例。您是说第二个对象应该更新几毫秒前刚刚更新的对象吗?它们是同一个对象吗?尝试同步数据库插入对我来说似乎是代码异味。
-
@ScaryWombat 是的,这就是这里应该发生的事情。任何具有相同 ID 的
Foo在概念上都是相同的对象。我试图遵守 REST 原则,即 REST 客户端不需要知道服务器的状态;这里的客户端只有 Foo #1 的一些信息,它应该能够通过串行或并行消息将该信息 POST 到服务器。 -
如果 foo-1 还不存在,其他调用者从哪里获取 id?
-
@NathanHughes ID 由外部来源提供。
标签: java spring spring-data-jpa spring-data database-concurrency