3

我有这个交易:

em.getTransaction().begin();
{
    final Payment payment = em.find(Payment.class, id);
    if (payment.status != Status.INIT)
        throw new IllegalStateException("Cannot set to PAID, is not INIT but " + status);

    payment.status = Status.PAID;

}
em.getTransaction().commit();
log.info("Payment " + id + " was paid");

但是,正如您在此处看到的,事务不会阻止竞争条件:

[11:10:18.265] INFO  [PaymentServlet] [MSP] Status COMPLETED 
[11:10:18.265] INFO  [PaymentServlet] Payment c76f9e75-99d7-4721-a8ac-e3a638dd8317 was paid 
[11:10:18.267] INFO  [PaymentServlet] [MSP] Status COMPLETED 
[11:10:18.267] INFO  [PaymentServlet] Payment c76f9e75-99d7-4721-a8ac-e3a638dd8317 was paid 

付款设置为PAID两次。我的异常没有被抛出,也没有回滚或任何东西。

我究竟做错了什么?

4

2 回答 2

7

您需要使用乐观锁定。乐观锁定是冲突更新很少发生的地方,因此可以在偶尔事务发生时回滚它。悲观锁定会导致数据库在对象使用时对其进行锁定,从而有效地单线程处理所有内容并可能导致性能问题。有关更详细的说明,请参阅http://en.wikibooks.org/wiki/Java_Persistence/Locking#JPA_2.0_Locking 。

要解决这里的问题,您应该在 Payment 中添加一个字段(传统声明为 private Long 版本)并为其提供 JPA @Version 注解。如果您手动管理架构,请确保相应的列存在于正确的表中。然后,JPA 将使用此字段来检查冲突更新并在存在冲突时回滚事务。

更新:有关悲观锁定的更多信息:https ://blogs.oracle.com/carolmcdonald/entry/jpa_2_0_concurrency_and简而言之,您可以配置 JPA 来锁定对象,但这样做是个好主意的情况非常罕见。换句话说,如果你手动编写 JDBC 查询,你必须在每次选择的末尾写上“for update”来导致悲观锁定;默认是不锁定读取,因为它会让数据库和数据库用户哭泣。

于 2012-05-15T09:27:35.300 回答
1

你没有说你正在使用什么数据库,或者什么事务隔离级别。如果您使用SERIALIZABLE符合 SQL 标准的事务,您将不会看到此错误。PostgreSQL 9.1 之前的版本,MS SQL Server 的一些配置,以及所有版本的 Oracle 在你请求的时候都不会给你真正的可序列化事务,所以在这样的环境中必须使用显式锁。大多数数据库产品默认为READ COMMITTED事务隔离级别,因此您可能需要显式请求SERIALIZABLE事务。

充分披露,我与麻省理工学院的 Dan RK Ports 合作,将真正的可序列化事务添加到 PostgreSQL 版本 9.1,以便 Wisconsin Courts 软件可以干净地处理这些问题。有关差异的示例,请参阅此 Wiki 页面

于 2012-05-15T12:17:42.340 回答