JPA 和乐观锁定模式

Posted

技术标签:

【中文标题】JPA 和乐观锁定模式【英文标题】:JPA and optimistic locking modes 【发布时间】:2012-11-14 22:03:03 【问题描述】:

我在 Oracle here 的博客上阅读了一篇关于 JPA 和锁定模式的文章。

我不完全理解OPTIMISTICOPTIMISTIC_FORCE_INCREMENT 锁定模式类型之间的区别。

OPTIMISTIC 模式:

当用户使用此模式锁定实体时,会在事务开始时检查版本字段实体 (@version),并在事务结束时检查版本字段。如果版本不同,则事务回滚。

OPTIMISTIC_FORCE_INCREMENT 模式:

当用户选择此模式时,他必须将 EntityManager 的状态刷新()到数据库中以手动增加版本字段。因此,所有其他乐观事务都将失效(回滚)。在事务结束时也会检查版本以提交或回滚事务。

似乎很清楚,但我应该何时使用 OPTIMISTICOPTIMISTIC_FORCE_INCREMENT 模式?我看到的唯一标准是当我希望事务优先于其他事务时应用 OPTIMISTIC_FORCE_INCREMENT 模式,因为选择此模式将回滚所有其他正在运行的事务(如果我理解机制的话)。

除了OPTIMISTIC 模式,还有其他理由选择这种模式吗?

谢谢

【问题讨论】:

我认为我们使用 OPTIMISTIC_FORCE_INCREMENT 来更新包含另一种类型的实体作为字段(@oneToOne,@oneToMany)的实体。因此,当我们想要更新 List 时,我们使用 OPTIMISTIC_FORCE_INCREMENT 锁定所有 ListItem。因此,在提交时,我们确保与我们要更新的实体链接的所有实体都保持不变。不是吗? 【参考方案1】:

不要被这个冗长的答案吓到。这个话题不简单。

默认情况下 JPA 强加 如果您未指定任何锁定,读取已提交隔离级别(与使用 LockModeType.NONE 的行为相同)。

已提交读要求脏读现象不存在。只是 T1 只能在 T2 提交后看到 T2 所做的更改。

在 JPA 中使用乐观锁定将隔离级别提高到 可重复读取

如果 T1 在事务开始和结束时读取一些数据,可重复读取确保 T1 看到相同的数据,即使 T2 更改数据并在 T1 中间提交。

这里是棘手的部分。 JPA 以最简单的方式实现可重复读取:通过防止 不可重复读取现象。 JPA 不够复杂,无法保留读取的快照。它只是通过引发异常来防止发生第二次读取(如果数据与第一次读取相比已更改)。

您可以从两个乐观锁定选项中进行选择:

LockModeType.OPTIMISTIC(JPA 1.0 中的LockModeType.READ

LockModeType.OPTIMISTIC_FORCE_INCREMENT(JPA 1.0 中的LockModeType.WRITE

两者有什么区别?

让我用这个Person实体的例子来说明。

@Entity
public class Person 
    @Id int id;
    @Version int version;
    String name;
    String label;
    @OneToMany(mappedBy = "person", fetch = FetchType.EAGER)
    List<Car> cars;
    // getters & setters

现在假设我们有一个名为 John 的人存储在数据库中。我们在 T1 中读取了这个人,但在第二次交易 T2 中将他的名字改为 Mike

没有任何锁定

Person person1 = em1.find(Person.class, id, LockModeType.NONE); //T1 reads Person("John")

Person person2 = em2.find(Person.class, id); //T2 reads Person("John")
person2.setName("Mike"); //Changing name to "Mike" within T2
em2.getTransaction().commit(); // T2 commits

System.out.println(em1.find(Person.class, id).getName()); // prints "John" - entity is already in Persistence cache
System.out.println(
  em1.createQuery("SELECT count(p) From Person p where p.name='John'")
    .getSingleResult()); // prints 0 - ups! don't know about any John (Non-repetable read)

乐观读锁

    Person person1 = em1.find(Person.class, id, LockModeType.OPTIMISTIC); //T1 reads Person("John")

    Person person2 = em2.find(Person.class, id); //T2 reads Person("John")
    person2.setName("Mike"); //Changing name to "Mike" within T2
    em2.getTransaction().commit(); // T2 commits

    System.out.println(
            em1.createQuery("SELECT count(p) From Person p where p.name='John'")
                    .getSingleResult()); // OptimisticLockException - The object [Person@2ac6f054] cannot be updated because it has changed or been deleted since it was last read. 


LockModeType.OPTIMISTIC_FORCE_INCREMENT 用于对其他实体进行更改(可能是非拥有关系)并且我们希望保持完整性。 让我以约翰购买一辆新车为例。

乐观读锁

Person john1 = em1.find(Person.class, id); //T1 reads Person("John")

Person john2 = em2.find(Person.class, id, LockModeType.OPTIMISTIC); //T2 reads Person("John")
//John gets a mercedes
Car mercedes = new Car();
mercedes.setPerson(john2);
em2.persist(mercedes);
john2.getCars().add(mercedes);
em2.getTransaction().commit(); // T2 commits

//T1 doesn't know about John's new car. john1 in stale state. We'll end up with wrong info about John.
if (john1.getCars().size() > 0)  
    john1.setLabel("John has a car");
 else 
    john1.setLabel("John doesn't have a car");

em1.flush();

乐观写锁

Person john1 = em1.find(Person.class, id); //T1 reads Person("John")
Person john2 = em2.find(Person.class, id, LockModeType.OPTIMISTIC_FORCE_INCREMENT); //T2 reads Person("John")
//John gets a mercedes
Car mercedes = new Car();
mercedes.setPerson(john2);
em2.persist(mercedes);
john2.getCars().add(mercedes);
em2.getTransaction().commit(); // T2 commits

//T1 doesn't know about John's new car. john1 in stale state. That's ok though because proper locking won't let us save wrong information about John.
if (john1.getCars().size() > 0)  
    john1.setLabel("John has a car");
 else 
    john1.setLabel("John doesn't have a car");

em1.flush(); // OptimisticLockException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect)

虽然 JPA 规范中有如下注释,但 Hibernate 和 EclipseLink 表现不错,不要使用它。

对于版本化对象,允许实现使用 LockMode- Type.OPTIMISTIC_FORCE_INCREMENT 其中 LockModeType.OPTIMISTIC 被请求, 但反之亦然。

【讨论】:

我认为在您的最后一个示例中,当您因为没有使用锁而执行 em1.flush() 时,我们应该不会遇到异常。实际上,因为您在实体中使用了版本:***.com/a/19872657/1059372 我针对 Hibernate 4.3.6.Final 和 Eclipselink 2.5.1 测试了我的示例。关于您的担忧:在 cmets 中可以看到休眠消息。 Eclipselink 打印 OptimisticLockException The object [Person@716d7daf] cannot be updated because it has changed or deleted since it was last read. Person 确实如第一次截断所示被版本化。 我检查了在Person 上没有@Vesion 列的情况下它会如何表现,它会在锁定时失败。带有 AssertionFailure 的休眠:无法强制在非版本化实体上增加版本 和带有 PersistenceException 的 Eclipselink:对于没有版本锁定索引的实体,锁定模式类型无效。没有版本锁定索引时,只能使用 PESSIMISTIC 锁定模式类型。 @zbig 嘿,你提到了乐观读锁的两个例子,在第一个例子中,em1 发生了 OptimisticLockException,因为 em2 已经更改了对象。但是,在第二个示例中,即使 em2 更改对象,您也没有提到 em1 的任何异常。请澄清.. “LockModeType.OPTIMISTIC”是否要求实体中存在版本列?【参考方案2】:

通常您永远不会使用 lock() API 进行乐观锁定。 JPA 将在任何更新或删除时自动检查任何版本列。

lock() API 用于乐观锁定的唯一目的是当您的更新依赖于另一个未更改/更新的对象时。如果其他对象发生更改,这允许您的事务仍然失败。

何时执行此操作取决于应用程序和用例。 OPTIMISTIC 将确保在您提交时其他对象尚未更新。 OPTIMISTIC_FORCE_INCREMENT 将确保其他对象尚未更新,并会在提交时增加其版本。

乐观锁定总是在提交时验证,并且在提交之前不能保证成功。您可以使用 flush() 提前强制数据库锁定,或触发更早的错误。

【讨论】:

NONE(我认为的默认模式)和 OPTIMISTIC 之间有什么区别?谢谢 flush() 是否保证在提交之前以对其他事务可见的方式递增版本?【参考方案3】:

LockModeType.OPTIMISTIC 存在check-then-act 问题,因此最好将其与PESSIMISTIC_READPESSIMISTIC_WRITE 耦合。

另一方面,LockModeType.OPTIMISTIC_FORCE_INCREMENT 没有任何数据不一致问题,您通常会在修改子实体时使用它来控制父实体的版本。

例如,您可以使用LockModeType.OPTIMISTIC_FORCE_INCREMENTLockModeType.PESSIMISTIC_FORCE_INCREMENT,以便parent entity version tales into consideration child entity changes as well。

【讨论】:

以上是关于JPA 和乐观锁定模式的主要内容,如果未能解决你的问题,请参考以下文章

Spring Data JDBC 中的乐观锁

乐观锁定的 JPA 版本字段的最佳类型

使用JPA和Hibernate重试乐观锁定异常

JPA 在带有 DTO 和乐观锁定的 RESTful Web 应用程序中合并?

使用 JPA / Hibernate 在无状态应用程序中进行乐观锁定

Oracle的悲观锁和乐观锁