JPA两个事务更新相同的行

时间:2017-08-07 10:01:16

标签: java mysql jpa concurrency transactions

我在我的SpringBoot应用程序中使用JPA存储库我有一个后台任务,定期将我的数据库中的某些元素设置为“准备好”。 我还有一个用户可以调用的端点,它可以修改同一个表中的一行。

有没有办法避免彼此取消彼此写?采取这种情况

Table: 
Key
id     name      is_ready

0)初始数据有一个键(1 no_name false)

1)后台任务启动并即将通过设置

修改表Key
is_ready to true
Key key = repo.findKeyByIsReady(false)
key.setIsReady(true)
repo.save(key) <--- does NOT yet execute this

2)用户调用api端点将密钥名称更改为&#34; new_name&#34;并完成

3)现在后台服务执行repo.save(key),最终数据为

1    no_name   true 

而不是

1    new_name   true

基本上后台任务已覆盖用户设置的密钥名称

有没有办法避免这种情况?交易如何在这里有所帮助?

2 个答案:

答案 0 :(得分:2)

这通常通过额外的锁来解决:

乐观锁定

您在第二个事务中检测到该行已被其他人更改。然后,您要么告诉用户并要求手动修复或尝试自动合并更改。

要实现它,您必须向表中添加其他列 - 版本。然后在更新行时,查询将如下所示:

UPDATE ..., version=old_version+1 WHERE id=old_id and version=old_version

如果WHERE关闭没有找到该行(因为其他人增加了版本),更改的行数将为0(JDBC从DB获取此信息)并且JPA会在这种情况下抛出错误。

在JPA中,必须将附加字段映射为@Version

悲观锁

每次更新实体时都使用构造:

select ... for update

当第二个事务发出这样的语句时,DB会锁定该请求,直到第一个事务完成。如果在特定时间段内没有发生这种情况,您将从JPA获得例外。有关详细信息,请参阅EntityManager#lock()方法。

答案 1 :(得分:0)

有几种方法可以解决这个问题 -

  1. 仅更新is_ready字段

    • 在后台任务中,不要更新记录的所有字段,而只更新is_ready字段。这样,您就不会覆盖对其他字段的更改
  2. 使用具有更严格的数据库隔离级别的事务锁定行 -

    • MySQL提供all standard database isolation levels
    • 您可以使用&#39; SERIALIZABLE&#39;隔离级别
    • 在此级别下,一旦启动事务并读取一行,在第一个事务提交(或回滚)之前,没有其他事务可以更新同一行
    • 因此,在您的情况下,即使两个进程都设法读取处于相同旧状态的行,也只有其中一个能够成功更新它。另一个进程无法更新记录(我自己没有对此进行过测试,但这就是它应该在这种隔离级别下工作的方式)。
    • 您可以使用Spring的@Transactional注释来标记隔离级别 - @Transactional(isolation=Isolation.SERIALIZABLE)
    • 请注意,使用这种严格的隔离级别时必须小心。涉及从多个表读取的错误用法可能会导致死锁