竞争条件虽然使用交易

时间:2012-05-15 09:20:05

标签: java hibernate jpa transactions race-condition

我有这个交易:

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两次。我没有抛出异常,也没有回滚或任何东西。

我做错了什么?

2 个答案:

答案 0 :(得分:7)

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

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

更新:有关悲观锁定的更多信息:https://blogs.oracle.com/carolmcdonald/entry/jpa_2_0_concurrency_and简而言之,可以配置JPA来锁定对象,但这样做是非常罕见的。换句话说,如果您是对JDBC进行手工编码查询,则必须在每次选择结束时写入“for update”以引起悲观锁定;默认情况下不会锁定读取,因为它会使数据库和数据库用户哭泣。

答案 1 :(得分:1)

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

完全披露,我与Dan R.K.合作。麻省理工学院的港口将真正的可序列化交易添加到PostgreSQL版本9.1,以便威斯康星州法院软件可以干净地处理这些问题。有关差异的示例,请参阅this Wiki page